From be977f6861acd6feb91e6dc301b9e75ee25b31bd Mon Sep 17 00:00:00 2001 From: Minh Huy Tran Date: Mon, 25 Mar 2024 09:56:48 +0100 Subject: [PATCH 1/6] feat(wire): add ecdsa authentication to wire Signed-off-by: Minh Huy Tran --- wallet/address.go | 4 +++- wire/account.go | 37 +++++++++++++++++++++++++++++++++---- wire/address.go | 32 ++++++++++++++++++++++++++------ wire/doc.go | 19 +++++++++++++++++++ wire/wire_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 wire/doc.go create mode 100644 wire/wire_test.go diff --git a/wallet/address.go b/wallet/address.go index 1898263..b2ff3f1 100644 --- a/wallet/address.go +++ b/wallet/address.go @@ -70,7 +70,9 @@ func (a *Address) Equal(addr wallet.Address) bool { } // Cmp checks ordering of two addresses. -// 0 if a==b, +// +// 0 if a==b, +// // -1 if a < b, // +1 if a > b. // https://godoc.org/bytes#Compare diff --git a/wire/account.go b/wire/account.go index 65f71d2..734093e 100644 --- a/wire/account.go +++ b/wire/account.go @@ -1,4 +1,4 @@ -// Copyright 2022 - See NOTICE file for copyright holders. +// Copyright 2024 - See NOTICE file for copyright holders. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,19 +15,40 @@ package wire import ( + "crypto/ecdsa" "math/rand" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/perun-network/perun-eth-backend/wallet" + "github.com/pkg/errors" "perun.network/go-perun/wire" ) +// SigLen length of a signature in byte. +// ref https://godoc.org/github.com/ethereum/go-ethereum/crypto/secp256k1#Sign +// ref https://github.com/ethereum/go-ethereum/blob/54b271a86dd748f3b0bcebeaf678dc34e0d6177a/crypto/signature_cgo.go#L66 +const SigLen = 65 + +// sigVSubtract value that is subtracted from the last byte of a signature if +// the last bytes exceeds it. +const sigVSubtract = 27 + // Account is a wire account. type Account struct { addr *Address + key *ecdsa.PrivateKey } // Sign signs the given message with the account's private key. -func (acc *Account) Sign(_ []byte) ([]byte, error) { - return []byte("Authenticate"), nil +func (acc *Account) Sign(data []byte) ([]byte, error) { + hash := PrefixedHash(data) + sig, err := crypto.Sign(hash, acc.key) + if err != nil { + return nil, errors.Wrap(err, "SignHash") + } + sig[64] += 27 + return sig, nil } // Address returns the account's address. @@ -37,7 +58,15 @@ func (acc *Account) Address() wire.Address { // NewRandomAccount generates a new random account. func NewRandomAccount(rng *rand.Rand) *Account { + privateKey, err := ecdsa.GenerateKey(secp256k1.S256(), rng) + if err != nil { + panic(err) + } + + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + return &Account{ - addr: NewRandomAddress(rng), + addr: &Address{wallet.AsWalletAddr(addr)}, + key: privateKey, } } diff --git a/wire/address.go b/wire/address.go index bfa8325..e615e8b 100644 --- a/wire/address.go +++ b/wire/address.go @@ -15,12 +15,12 @@ package wire import ( - "bytes" - "errors" "math/rand" + "github.com/ethereum/go-ethereum/crypto" "github.com/perun-network/perun-eth-backend/wallet" "github.com/perun-network/perun-eth-backend/wallet/test" + "github.com/pkg/errors" "perun.network/go-perun/wire" ) @@ -32,7 +32,7 @@ type Address struct { // NewAddress returns a new address. func NewAddress() *Address { - return &Address{} + return &Address{Address: &wallet.Address{}} } // Equal returns whether the two addresses are equal. @@ -41,6 +41,7 @@ func (a Address) Equal(b wire.Address) bool { if !ok { panic("wrong type") } + return a.Address.Equal(bTyped.Address) } @@ -62,9 +63,28 @@ func NewRandomAddress(rng *rand.Rand) *Address { // Verify verifies a message signature. // It returns an error if the signature is invalid. -func (a Address) Verify(_ []byte, sig []byte) error { - if !bytes.Equal(sig, []byte("Authenticate")) { - return errors.New("invalid signature") +func (a Address) Verify(msg []byte, sig []byte) error { + hash := PrefixedHash(msg) + sigCopy := make([]byte, SigLen) + copy(sigCopy, sig) + if len(sigCopy) == SigLen && (sigCopy[SigLen-1] >= sigVSubtract) { + sigCopy[SigLen-1] -= sigVSubtract + } + pk, err := crypto.SigToPub(hash, sigCopy) + if err != nil { + return errors.WithStack(err) + } + addr := crypto.PubkeyToAddress(*pk) + if !a.Equal(&Address{wallet.AsWalletAddr(addr)}) { + return errors.New("signature verification failed") } return nil } + +// PrefixedHash adds an ethereum specific prefix to the hash of given data, rehashes the results +// and returns it. +func PrefixedHash(data []byte) []byte { + hash := crypto.Keccak256(data) + prefix := []byte("\x19Ethereum Signed Message:\n32") + return crypto.Keccak256(prefix, hash) +} diff --git a/wire/doc.go b/wire/doc.go new file mode 100644 index 0000000..52d326a --- /dev/null +++ b/wire/doc.go @@ -0,0 +1,19 @@ +// Copyright 2024 - See NOTICE file for copyright holders. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package wire contains a simplistic implementation of the perun wire's +// account, and address interfaces. +// An account can be instantiated directly with a random secret key. +// The account and address offer Handshake Authentication through Go-Perun Wire. +package wire // import "github.com/perun-network/perun-eth-backend/wire" diff --git a/wire/wire_test.go b/wire/wire_test.go new file mode 100644 index 0000000..db99c88 --- /dev/null +++ b/wire/wire_test.go @@ -0,0 +1,45 @@ +// Copyright 2024 - See NOTICE file for copyright holders. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wire_test + +import ( + "math/rand" + "testing" + + "github.com/perun-network/perun-eth-backend/wire" + "github.com/stretchr/testify/assert" + perunwire "perun.network/go-perun/wire" + "perun.network/go-perun/wire/test" + pkgtest "polycry.pt/poly-go/test" +) + +var dataToSign = []byte("SomeLongDataThatShouldBeSignedPlease") + +func TestAddress(t *testing.T) { + test.TestAddressImplementation(t, func() perunwire.Address { + return wire.NewAddress() + }, func(rng *rand.Rand) perunwire.Address { + return wire.NewRandomAddress(rng) + }) +} +func TestSignatures(t *testing.T) { + acc := wire.NewRandomAccount(pkgtest.Prng(t)) + sig, err := acc.Sign(dataToSign) + assert.NoError(t, err, "Sign with new account should succeed") + assert.NotNil(t, sig) + assert.Equal(t, len(sig), wire.SigLen, "Ethereum signature has wrong length") + err = acc.Address().Verify(dataToSign, sig) + assert.NoError(t, err, "Verification should succeed") +} From 6a03dc2ad013c2a92b22f301bc2d745ecc9e99b5 Mon Sep 17 00:00:00 2001 From: Minh Huy Tran Date: Mon, 25 Mar 2024 10:12:06 +0100 Subject: [PATCH 2/6] chore(lint): fix linting Signed-off-by: Minh Huy Tran --- wire/wire_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/wire/wire_test.go b/wire/wire_test.go index db99c88..ce7ee3a 100644 --- a/wire/wire_test.go +++ b/wire/wire_test.go @@ -34,6 +34,7 @@ func TestAddress(t *testing.T) { return wire.NewRandomAddress(rng) }) } + func TestSignatures(t *testing.T) { acc := wire.NewRandomAccount(pkgtest.Prng(t)) sig, err := acc.Sign(dataToSign) From 73eef3aadb6e980fd57e253c07a4033faed65375 Mon Sep 17 00:00:00 2001 From: Minh Huy Tran Date: Tue, 2 Jul 2024 13:02:54 +0200 Subject: [PATCH 3/6] feat: add client wire test Signed-off-by: Minh Huy Tran --- client/client_net_test.go | 181 ++++++++++++++++++++++++++++++++++++++ client/test/setup.go | 94 +++++++++++++++++++- go.mod | 2 +- go.sum | 4 + wire/address.go | 6 ++ wire/init_test.go | 19 ++++ wire/wire_test.go | 59 ++++++++++++- 7 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 client/client_net_test.go create mode 100644 wire/init_test.go diff --git a/client/client_net_test.go b/client/client_net_test.go new file mode 100644 index 0000000..7adc2a8 --- /dev/null +++ b/client/client_net_test.go @@ -0,0 +1,181 @@ +// Copyright 2020 - See NOTICE file for copyright holders. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package client_test + +import ( + "context" + "math/big" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + ethchannel "github.com/perun-network/perun-eth-backend/channel" + "github.com/perun-network/perun-eth-backend/channel/test" + ctest "github.com/perun-network/perun-eth-backend/client/test" + ethwallet "github.com/perun-network/perun-eth-backend/wallet" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "perun.network/go-perun/channel" + chtest "perun.network/go-perun/channel/test" + "perun.network/go-perun/client" + clienttest "perun.network/go-perun/client/test" + "perun.network/go-perun/log" + "perun.network/go-perun/wire" + pkgtest "polycry.pt/poly-go/test" +) + +func TestNetProgression(t *testing.T) { + rng := pkgtest.Prng(t) + + names := []string{"Paul", "Paula"} + backendSetup := test.NewSetup(t, rng, 2, ctest.BlockInterval, TxFinalityDepth) + roleSetups := ctest.MakeNetRoleSetups(t, rng, backendSetup, names) + clients := [2]clienttest.Executer{ + clienttest.NewPaul(t, roleSetups[0]), + clienttest.NewPaula(t, roleSetups[1]), + } + + appAddress := deployMockApp(t, backendSetup) + appAddrBackend := appAddress.(*ethwallet.Address) + appID := ðchannel.AppID{Address: appAddrBackend} + app := channel.NewMockApp(appID) + channel.RegisterApp(app) + + execConfig := &clienttest.ProgressionExecConfig{ + BaseExecConfig: clienttest.MakeBaseExecConfig( + clientAddresses(roleSetups), + backendSetup.Asset, + [2]*big.Int{big.NewInt(99), big.NewInt(1)}, + client.WithApp(app, channel.NewMockOp(channel.OpValid)), + ), + } + + ctx, cancel := context.WithTimeout(context.Background(), twoPartyTestTimeout) + defer cancel() + clienttest.ExecuteTwoPartyTest(ctx, t, clients, execConfig) +} + +func TestNetPaymentHappy(t *testing.T) { + log.Info("Starting happy test") + rng := pkgtest.Prng(t) + + const A, B = 0, 1 // Indices of Alice and Bob + var ( + name = [2]string{"Alice", "Bob"} + role [2]clienttest.Executer + ) + + s := test.NewSetup(t, rng, 2, ctest.BlockInterval, TxFinalityDepth) + setup := ctest.MakeNetRoleSetups(t, rng, s, name[:]) + + role[A] = clienttest.NewAlice(t, setup[A]) + role[B] = clienttest.NewBob(t, setup[B]) + // enable stages synchronization + stages := role[A].EnableStages() + role[B].SetStages(stages) + + execConfig := &clienttest.AliceBobExecConfig{ + BaseExecConfig: clienttest.MakeBaseExecConfig( + [2]wire.Address{setup[A].Identity.Address(), setup[B].Identity.Address()}, + s.Asset, + [2]*big.Int{big.NewInt(100), big.NewInt(100)}, + client.WithApp(chtest.NewRandomAppAndData(rng)), + ), + NumPayments: [2]int{2, 2}, + TxAmounts: [2]*big.Int{big.NewInt(5), big.NewInt(3)}, + } + + var wg sync.WaitGroup + wg.Add(2) + for i := 0; i < 2; i++ { + go func(i int) { + defer wg.Done() + log.Infof("Starting %s.Execute", name[i]) + role[i].Execute(execConfig) + }(i) + } + + wg.Wait() + + // Assert correct final balances + aliceToBob := big.NewInt(int64(execConfig.NumPayments[A])*execConfig.TxAmounts[A].Int64() - + int64(execConfig.NumPayments[B])*execConfig.TxAmounts[B].Int64()) + finalBalAlice := new(big.Int).Sub(execConfig.InitBals()[A], aliceToBob) + finalBalBob := new(big.Int).Add(execConfig.InitBals()[B], aliceToBob) + // reset context timeout + ctx, cancel := context.WithTimeout(context.Background(), ctest.DefaultTimeout) + defer cancel() + assertBal := func(addr *ethwallet.Address, bal *big.Int) { + b, err := s.SimBackend.BalanceAt(ctx, common.Address(*addr), nil) + require.NoError(t, err) + assert.Zero(t, bal.Cmp(b), "ETH balance mismatch") + } + + assertBal(s.Recvs[A], finalBalAlice) + assertBal(s.Recvs[B], finalBalBob) + + log.Info("Happy test done") +} + +func TestNetPaymentDispute(t *testing.T) { + log.Info("Starting dispute test") + rng := pkgtest.Prng(t) + + const A, B = 0, 1 // Indices of Mallory and Carol + var ( + name = [2]string{"Mallory", "Carol"} + role [2]clienttest.Executer + ) + + s := test.NewSetup(t, rng, 2, ctest.BlockInterval, TxFinalityDepth) + setup := ctest.MakeNetRoleSetups(t, rng, s, name[:]) + + role[A] = clienttest.NewMallory(t, setup[A]) + role[B] = clienttest.NewCarol(t, setup[B]) + + execConfig := &clienttest.MalloryCarolExecConfig{ + BaseExecConfig: clienttest.MakeBaseExecConfig( + [2]wire.Address{setup[A].Identity.Address(), setup[B].Identity.Address()}, + s.Asset, + [2]*big.Int{big.NewInt(100), big.NewInt(1)}, + client.WithoutApp(), + ), + NumPayments: [2]int{5, 0}, + TxAmounts: [2]*big.Int{big.NewInt(20), big.NewInt(0)}, + } + + ctx, cancel := context.WithTimeout(context.Background(), twoPartyTestTimeout) + defer cancel() + clienttest.ExecuteTwoPartyTest(ctx, t, role, execConfig) + + // Assert correct final balances + netTransfer := big.NewInt(int64(execConfig.NumPayments[A])*execConfig.TxAmounts[A].Int64() - + int64(execConfig.NumPayments[B])*execConfig.TxAmounts[B].Int64()) + finalBal := [2]*big.Int{ + new(big.Int).Sub(execConfig.InitBals()[A], netTransfer), + new(big.Int).Add(execConfig.InitBals()[B], netTransfer), + } + // reset context timeout + ctx, cancel = context.WithTimeout(context.Background(), ctest.DefaultTimeout) + defer cancel() + for i, bal := range finalBal { + b, err := s.SimBackend.BalanceAt(ctx, common.Address(*s.Recvs[i]), nil) + require.NoError(t, err) + assert.Zero(t, b.Cmp(bal), "ETH balance mismatch") + } + + log.Info("Dispute test done") +} diff --git a/client/test/setup.go b/client/test/setup.go index 0095fe4..363bec6 100644 --- a/client/test/setup.go +++ b/client/test/setup.go @@ -1,4 +1,4 @@ -// Copyright 2021 - See NOTICE file for copyright holders. +// Copyright 2024 - See NOTICE file for copyright holders. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,15 +15,23 @@ package test import ( + "crypto/tls" + "fmt" "math/rand" + "net" + "testing" "time" ethctest "github.com/perun-network/perun-eth-backend/channel/test" ethwtest "github.com/perun-network/perun-eth-backend/wallet/test" + "github.com/stretchr/testify/assert" clienttest "perun.network/go-perun/client/test" "perun.network/go-perun/watcher/local" "perun.network/go-perun/wire" + perunnet "perun.network/go-perun/wire/net" + "perun.network/go-perun/wire/net/simple" + perunio "perun.network/go-perun/wire/perunio/serializer" wiretest "perun.network/go-perun/wire/test" ) @@ -34,6 +42,9 @@ const ( BlockInterval = 200 * time.Millisecond // challenge duration in blocks that is used by MakeRoleSetups. challengeDurationBlocks = 90 + + // dialerTimeout is the timeout for dialing a connection. + dialerTimeout = 15 * time.Second ) // MakeRoleSetups creates a two party client test setup with the provided names. @@ -62,3 +73,84 @@ func MakeRoleSetups(rng *rand.Rand, s *ethctest.Setup, names []string) []clientt } return setups } + +// MakeNetRoleSetups creates a two party client test setup with the provided names and uses the default TLS-bus from go-perun. +func MakeNetRoleSetups(t *testing.T, rng *rand.Rand, s *ethctest.Setup, names []string) []clienttest.RoleSetup { + setups := make([]clienttest.RoleSetup, len(names)) + commonName := "127.0.0.1" + sans := []string{"127.0.0.1", "localhost"} + tlsConfigs, err := simple.GenerateSelfSignedCertConfigs(commonName, sans, len(names)) + if err != nil { + panic("Error generating TLS configs: " + err.Error()) + } + + hosts := make([]string, len(names)) + for i := 0; i < len(names); i++ { + port, err := findFreePort() + assert.NoError(t, err, "Error finding free port") + hosts[i] = fmt.Sprintf("127.0.0.1:%d", port) + } + + dialers, listeners := makeSimpleDialersListeners(t, tlsConfigs, hosts) + + for i := 0; i < len(setups); i++ { + watcher, err := local.NewWatcher(s.Adjs[i]) + if err != nil { + panic("Error initializing watcher: " + err.Error()) + } + + acc := wiretest.NewRandomAccount(rng) + + for j := 0; j < len(setups); j++ { + dialers[j].Register(acc.Address(), hosts[i]) + } + + bus := perunnet.NewBus(acc, dialers[i], perunio.Serializer()) + go bus.Listen(listeners[i]) + + setups[i] = clienttest.RoleSetup{ + Name: names[i], + Identity: acc, + Bus: bus, + Funder: s.Funders[i], + Adjudicator: s.Adjs[i], + Watcher: watcher, + Wallet: ethwtest.NewTmpWallet(), + Timeout: DefaultTimeout, + // Scaled due to simbackend automining progressing faster than real time. + ChallengeDuration: challengeDurationBlocks * uint64(time.Second/BlockInterval), + Errors: make(chan error), + BalanceReader: s.SimBackend.NewBalanceReader(s.Accs[i].Address()), + } + + } + + return setups +} + +func makeSimpleDialersListeners(t *testing.T, tlsConfigs []*tls.Config, hosts []string) ([]*simple.Dialer, []*simple.Listener) { + dialers := make([]*simple.Dialer, len(tlsConfigs)) + listeners := make([]*simple.Listener, len(tlsConfigs)) + + var err error + for i, tlsConfig := range tlsConfigs { + dialers[i] = simple.NewTCPDialer(dialerTimeout, tlsConfig) + listeners[i], err = simple.NewTCPListener(hosts[i], tlsConfig) + assert.NoError(t, err, "Error creating listener") + } + + return dialers, listeners +} + +func findFreePort() (int, error) { + // Create a listener on a random port to get an available port. + l, err := net.Listen("tcp", ":0") // Use ":0" to bind to a random free port + if err != nil { + return 0, err + } + defer l.Close() + + // Get the port from the listener's address + addr := l.Addr().(*net.TCPAddr) + return addr.Port, nil +} diff --git a/go.mod b/go.mod index 78305ea..a3e8891 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - perun.network/go-perun v0.11.1-0.20240312125059-3811fdb76baf + perun.network/go-perun v0.11.1-0.20240702085953-7002772cf1ce polycry.pt/poly-go v0.0.0-20220301085937-fb9d71b45a37 ) diff --git a/go.sum b/go.sum index 16b4947..7bd989e 100644 --- a/go.sum +++ b/go.sum @@ -647,6 +647,10 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= perun.network/go-perun v0.11.1-0.20240312125059-3811fdb76baf h1:eTmgN9p2Uo48c+8Jiya67wnRakZEBoXlG3fWOSXlPw0= perun.network/go-perun v0.11.1-0.20240312125059-3811fdb76baf/go.mod h1:VfCebjZTnFrQRcjbkK1s1l+H71MKW7jodpFQBdP7tRQ= +perun.network/go-perun v0.11.1-0.20240702081131-cb43155f8c80 h1:xwNXTc6yyv5ch/MQe9QKzLyWS14EaS6hc00x/agd+gc= +perun.network/go-perun v0.11.1-0.20240702081131-cb43155f8c80/go.mod h1:VfCebjZTnFrQRcjbkK1s1l+H71MKW7jodpFQBdP7tRQ= +perun.network/go-perun v0.11.1-0.20240702085953-7002772cf1ce h1:ADv90PmdR39SHZ+iINHWv7CJnI6kJyCiwvM9GcMRFzw= +perun.network/go-perun v0.11.1-0.20240702085953-7002772cf1ce/go.mod h1:VfCebjZTnFrQRcjbkK1s1l+H71MKW7jodpFQBdP7tRQ= polycry.pt/poly-go v0.0.0-20220301085937-fb9d71b45a37 h1:iA5GzEa/hHfVlQpimEjPV09NATwHXxSjWNB0VVodtew= polycry.pt/poly-go v0.0.0-20220301085937-fb9d71b45a37/go.mod h1:XUBrNtqgEhN3EEOP/5gh7IBd3xVHKidCjXDZfl9+kMU= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/wire/address.go b/wire/address.go index e615e8b..855be4c 100644 --- a/wire/address.go +++ b/wire/address.go @@ -78,6 +78,12 @@ func (a Address) Verify(msg []byte, sig []byte) error { if !a.Equal(&Address{wallet.AsWalletAddr(addr)}) { return errors.New("signature verification failed") } + + // Verify the signature + if !crypto.VerifySignature(crypto.FromECDSAPub(pk), hash, sigCopy[:len(sigCopy)-1]) { + return errors.New("signature verification failed") + } + return nil } diff --git a/wire/init_test.go b/wire/init_test.go new file mode 100644 index 0000000..61cdad8 --- /dev/null +++ b/wire/init_test.go @@ -0,0 +1,19 @@ +// Copyright 2024 - See NOTICE file for copyright holders. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wire_test + +import ( + _ "github.com/perun-network/perun-eth-backend/wire" // import wire for testing +) diff --git a/wire/wire_test.go b/wire/wire_test.go index ce7ee3a..1e346f2 100644 --- a/wire/wire_test.go +++ b/wire/wire_test.go @@ -35,7 +35,7 @@ func TestAddress(t *testing.T) { }) } -func TestSignatures(t *testing.T) { +func TestSignatures_Success(t *testing.T) { acc := wire.NewRandomAccount(pkgtest.Prng(t)) sig, err := acc.Sign(dataToSign) assert.NoError(t, err, "Sign with new account should succeed") @@ -44,3 +44,60 @@ func TestSignatures(t *testing.T) { err = acc.Address().Verify(dataToSign, sig) assert.NoError(t, err, "Verification should succeed") } + +func TestSignatures_ModifyData_Failure(t *testing.T) { + acc := wire.NewRandomAccount(pkgtest.Prng(t)) + sig, err := acc.Sign(dataToSign) + assert.NoError(t, err, "Sign with new account should succeed") + assert.NotNil(t, sig) + + // Modify a single byte of the signed data + modifiedData := make([]byte, len(dataToSign)) + copy(modifiedData, dataToSign) + modifiedData[0] ^= 0x01 + + err = acc.Address().Verify(modifiedData, sig) + assert.Error(t, err, "Verification should fail with modified data") +} + +func TestSignatures_ModifySignature_Failure(t *testing.T) { + acc := wire.NewRandomAccount(pkgtest.Prng(t)) + sig, err := acc.Sign(dataToSign) + assert.NoError(t, err, "Sign with new account should succeed") + assert.NotNil(t, sig) + + // Modify a single byte of the signature (first 64 bytes) + modifiedSig := make([]byte, len(sig)) + copy(modifiedSig, sig) + modifiedSig[0] ^= 0x01 + + err = acc.Address().Verify(dataToSign, modifiedSig) + assert.Error(t, err, "Verification should fail with modified signature") +} + +func TestSignatures_ModifyLastByteOfSignature_Failure(t *testing.T) { + acc := wire.NewRandomAccount(pkgtest.Prng(t)) + sig, err := acc.Sign(dataToSign) + assert.NoError(t, err, "Sign with new account should succeed") + assert.NotNil(t, sig) + + // Modify the last byte of the signature + modifiedSig := make([]byte, len(sig)) + copy(modifiedSig, sig) + modifiedSig[len(sig)-1] ^= 0x01 + + err = acc.Address().Verify(dataToSign, modifiedSig) + assert.Error(t, err, "Verification should fail with modified signature") +} + +func TestSignatures_WrongAccount_Failure(t *testing.T) { + acc := wire.NewRandomAccount(pkgtest.Prng(t)) + sig, err := acc.Sign(dataToSign) + assert.NoError(t, err, "Sign with new account should succeed") + assert.NotNil(t, sig) + + // Verify with a wrong account + wrongAcc := wire.NewRandomAccount(pkgtest.Prng(t)) + err = wrongAcc.Address().Verify(dataToSign, sig) + assert.Error(t, err, "Verification should fail with wrong account") +} From 4c964438ce9e2ba40c96a539fcee02a5421c38e6 Mon Sep 17 00:00:00 2001 From: Minh Huy Tran Date: Tue, 2 Jul 2024 13:08:11 +0200 Subject: [PATCH 4/6] chore: update go.mod Signed-off-by: Minh Huy Tran --- go.mod | 2 +- go.sum | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index a3e8891..48bf91d 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - perun.network/go-perun v0.11.1-0.20240702085953-7002772cf1ce + perun.network/go-perun v0.11.1-0.20240326094100-011cfdf0ea51 polycry.pt/poly-go v0.0.0-20220301085937-fb9d71b45a37 ) diff --git a/go.sum b/go.sum index 7bd989e..ce9296c 100644 --- a/go.sum +++ b/go.sum @@ -645,12 +645,8 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -perun.network/go-perun v0.11.1-0.20240312125059-3811fdb76baf h1:eTmgN9p2Uo48c+8Jiya67wnRakZEBoXlG3fWOSXlPw0= -perun.network/go-perun v0.11.1-0.20240312125059-3811fdb76baf/go.mod h1:VfCebjZTnFrQRcjbkK1s1l+H71MKW7jodpFQBdP7tRQ= -perun.network/go-perun v0.11.1-0.20240702081131-cb43155f8c80 h1:xwNXTc6yyv5ch/MQe9QKzLyWS14EaS6hc00x/agd+gc= -perun.network/go-perun v0.11.1-0.20240702081131-cb43155f8c80/go.mod h1:VfCebjZTnFrQRcjbkK1s1l+H71MKW7jodpFQBdP7tRQ= -perun.network/go-perun v0.11.1-0.20240702085953-7002772cf1ce h1:ADv90PmdR39SHZ+iINHWv7CJnI6kJyCiwvM9GcMRFzw= -perun.network/go-perun v0.11.1-0.20240702085953-7002772cf1ce/go.mod h1:VfCebjZTnFrQRcjbkK1s1l+H71MKW7jodpFQBdP7tRQ= +perun.network/go-perun v0.11.1-0.20240326094100-011cfdf0ea51 h1:fIM4nKc6OKkBX/gJrCTMMFmxeoFTE1DAerndDW/njVc= +perun.network/go-perun v0.11.1-0.20240326094100-011cfdf0ea51/go.mod h1:VfCebjZTnFrQRcjbkK1s1l+H71MKW7jodpFQBdP7tRQ= polycry.pt/poly-go v0.0.0-20220301085937-fb9d71b45a37 h1:iA5GzEa/hHfVlQpimEjPV09NATwHXxSjWNB0VVodtew= polycry.pt/poly-go v0.0.0-20220301085937-fb9d71b45a37/go.mod h1:XUBrNtqgEhN3EEOP/5gh7IBd3xVHKidCjXDZfl9+kMU= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= From 03ce1db5455fbf1668032342273e3f7021f17102 Mon Sep 17 00:00:00 2001 From: Minh Huy Tran Date: Tue, 2 Jul 2024 13:47:58 +0200 Subject: [PATCH 5/6] feat: add self_signed_certificates for client net test Signed-off-by: Minh Huy Tran --- client/test/setup.go | 8 ++-- client/test/tls.go | 112 +++++++++++++++++++++++++++++++++++++++++++ wire/init_test.go | 20 +++++++- 3 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 client/test/tls.go diff --git a/client/test/setup.go b/client/test/setup.go index 363bec6..a27ca33 100644 --- a/client/test/setup.go +++ b/client/test/setup.go @@ -76,10 +76,11 @@ func MakeRoleSetups(rng *rand.Rand, s *ethctest.Setup, names []string) []clientt // MakeNetRoleSetups creates a two party client test setup with the provided names and uses the default TLS-bus from go-perun. func MakeNetRoleSetups(t *testing.T, rng *rand.Rand, s *ethctest.Setup, names []string) []clienttest.RoleSetup { + t.Helper() setups := make([]clienttest.RoleSetup, len(names)) commonName := "127.0.0.1" sans := []string{"127.0.0.1", "localhost"} - tlsConfigs, err := simple.GenerateSelfSignedCertConfigs(commonName, sans, len(names)) + tlsConfigs, err := GenerateSelfSignedCertConfigs(commonName, sans, len(names)) if err != nil { panic("Error generating TLS configs: " + err.Error()) } @@ -122,13 +123,12 @@ func MakeNetRoleSetups(t *testing.T, rng *rand.Rand, s *ethctest.Setup, names [] Errors: make(chan error), BalanceReader: s.SimBackend.NewBalanceReader(s.Accs[i].Address()), } - } - return setups } func makeSimpleDialersListeners(t *testing.T, tlsConfigs []*tls.Config, hosts []string) ([]*simple.Dialer, []*simple.Listener) { + t.Helper() dialers := make([]*simple.Dialer, len(tlsConfigs)) listeners := make([]*simple.Listener, len(tlsConfigs)) @@ -144,7 +144,7 @@ func makeSimpleDialersListeners(t *testing.T, tlsConfigs []*tls.Config, hosts [] func findFreePort() (int, error) { // Create a listener on a random port to get an available port. - l, err := net.Listen("tcp", ":0") // Use ":0" to bind to a random free port + l, err := net.Listen("tcp", "127.0.0.1:0") // Use ":0" to bind to a random free port if err != nil { return 0, err } diff --git a/client/test/tls.go b/client/test/tls.go new file mode 100644 index 0000000..dfc57d4 --- /dev/null +++ b/client/test/tls.go @@ -0,0 +1,112 @@ +// Copyright 2022 - See NOTICE file for copyright holders. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "time" +) + +const ( + timeout = 24 * time.Hour +) + +// GenerateSelfSignedCertConfigs generates self-signed certificates and returns +// a list of TLS configurations for n clients. +func GenerateSelfSignedCertConfigs(commonName string, sans []string, numClients int) ([]*tls.Config, error) { + keySize := 2048 + configs := make([]*tls.Config, numClients) + certPEMs := make([][]byte, numClients) + tlsCerts := make([]tls.Certificate, numClients) + + for i := 0; i < numClients; i++ { + // Private key for the client + privateKey, err := rsa.GenerateKey(rand.Reader, keySize) + if err != nil { + return nil, err + } + + // Create a certificate template + template := x509.Certificate{ + SerialNumber: big.NewInt(int64(i) + 1), + Subject: pkix.Name{ + Organization: []string{"Perun Network"}, + CommonName: fmt.Sprintf("%s-client-%d", commonName, i+1), + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(timeout), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + // Add SANs to the server certificate template + for _, san := range sans { + if ip := net.ParseIP(san); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, san) + } + } + + // Generate a self-signed server certificate + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, err + } + + // Encode the server certificate to PEM format + certPEMs[i] = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + // Encode the server private key to PEM format + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) + + // Create a tls.Certificate object for the server + tlsCerts[i], err = tls.X509KeyPair(certPEMs[i], keyPEM) + if err != nil { + return nil, err + } + } + + for i := 0; i < numClients; i++ { + certPool := x509.NewCertPool() + for j := 0; j < numClients; j++ { + ok := certPool.AppendCertsFromPEM(certPEMs[j]) + if !ok { + return nil, errors.New("failed to parse root certificate") + } + } + + // Create the server-side TLS configuration + configs[i] = &tls.Config{ + RootCAs: certPool, + ClientCAs: certPool, + Certificates: []tls.Certificate{tlsCerts[i]}, + ClientAuth: tls.RequireAndVerifyClientCert, + MinVersion: tls.VersionTLS12, // Set minimum TLS version to TLS 1.2 + } + } + + return configs, nil +} diff --git a/wire/init_test.go b/wire/init_test.go index 61cdad8..a9b4b77 100644 --- a/wire/init_test.go +++ b/wire/init_test.go @@ -15,5 +15,23 @@ package wire_test import ( - _ "github.com/perun-network/perun-eth-backend/wire" // import wire for testing + "math/rand" + + ethwire "github.com/perun-network/perun-eth-backend/wire" // import wire for testing + "perun.network/go-perun/wire" + wiretest "perun.network/go-perun/wire/test" ) + +func init() { + wire.SetNewAddressFunc(func() wire.Address { + return ethwire.NewAddress() + }) + + wiretest.SetNewRandomAddress(func(rng *rand.Rand) wire.Address { + return ethwire.NewRandomAddress(rng) + }) + + wiretest.SetNewRandomAccount(func(rng *rand.Rand) wire.Account { + return ethwire.NewRandomAccount(rng) + }) +} From 81b5fc8eecc2081416548c5cbb2e147eb173dc73 Mon Sep 17 00:00:00 2001 From: Minh Huy Tran Date: Tue, 2 Jul 2024 14:30:06 +0200 Subject: [PATCH 6/6] fix(wire): move wire_test to package wire to prevent init dependency Signed-off-by: Minh Huy Tran --- wire/account.go | 2 ++ wire/address.go | 8 +++++--- wire/init_test.go | 37 ------------------------------------- wire/wire_test.go | 22 ++++++++++++---------- 4 files changed, 19 insertions(+), 50 deletions(-) delete mode 100644 wire/init_test.go diff --git a/wire/account.go b/wire/account.go index 734093e..d2620db 100644 --- a/wire/account.go +++ b/wire/account.go @@ -16,6 +16,7 @@ package wire import ( "crypto/ecdsa" + "log" "math/rand" "github.com/ethereum/go-ethereum/crypto" @@ -62,6 +63,7 @@ func NewRandomAccount(rng *rand.Rand) *Account { if err != nil { panic(err) } + log.Print("Generated new account with address ", crypto.PubkeyToAddress(privateKey.PublicKey).Hex()) addr := crypto.PubkeyToAddress(privateKey.PublicKey) diff --git a/wire/address.go b/wire/address.go index 855be4c..2c08cb1 100644 --- a/wire/address.go +++ b/wire/address.go @@ -15,6 +15,7 @@ package wire import ( + "log" "math/rand" "github.com/ethereum/go-ethereum/crypto" @@ -36,7 +37,7 @@ func NewAddress() *Address { } // Equal returns whether the two addresses are equal. -func (a Address) Equal(b wire.Address) bool { +func (a *Address) Equal(b wire.Address) bool { bTyped, ok := b.(*Address) if !ok { panic("wrong type") @@ -47,7 +48,7 @@ func (a Address) Equal(b wire.Address) bool { // Cmp compares the byte representation of two addresses. For `a.Cmp(b)` // returns -1 if a < b, 0 if a == b, 1 if a > b. -func (a Address) Cmp(b wire.Address) int { +func (a *Address) Cmp(b wire.Address) int { bTyped, ok := b.(*Address) if !ok { panic("wrong type") @@ -63,7 +64,8 @@ func NewRandomAddress(rng *rand.Rand) *Address { // Verify verifies a message signature. // It returns an error if the signature is invalid. -func (a Address) Verify(msg []byte, sig []byte) error { +func (a *Address) Verify(msg []byte, sig []byte) error { + log.Print("Address.Verify called") hash := PrefixedHash(msg) sigCopy := make([]byte, SigLen) copy(sigCopy, sig) diff --git a/wire/init_test.go b/wire/init_test.go deleted file mode 100644 index a9b4b77..0000000 --- a/wire/init_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 - See NOTICE file for copyright holders. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package wire_test - -import ( - "math/rand" - - ethwire "github.com/perun-network/perun-eth-backend/wire" // import wire for testing - "perun.network/go-perun/wire" - wiretest "perun.network/go-perun/wire/test" -) - -func init() { - wire.SetNewAddressFunc(func() wire.Address { - return ethwire.NewAddress() - }) - - wiretest.SetNewRandomAddress(func(rng *rand.Rand) wire.Address { - return ethwire.NewRandomAddress(rng) - }) - - wiretest.SetNewRandomAccount(func(rng *rand.Rand) wire.Account { - return ethwire.NewRandomAccount(rng) - }) -} diff --git a/wire/wire_test.go b/wire/wire_test.go index 1e346f2..042d394 100644 --- a/wire/wire_test.go +++ b/wire/wire_test.go @@ -18,7 +18,7 @@ import ( "math/rand" "testing" - "github.com/perun-network/perun-eth-backend/wire" + ethwire "github.com/perun-network/perun-eth-backend/wire" "github.com/stretchr/testify/assert" perunwire "perun.network/go-perun/wire" "perun.network/go-perun/wire/test" @@ -29,24 +29,24 @@ var dataToSign = []byte("SomeLongDataThatShouldBeSignedPlease") func TestAddress(t *testing.T) { test.TestAddressImplementation(t, func() perunwire.Address { - return wire.NewAddress() + return ethwire.NewAddress() }, func(rng *rand.Rand) perunwire.Address { - return wire.NewRandomAddress(rng) + return ethwire.NewRandomAddress(rng) }) } func TestSignatures_Success(t *testing.T) { - acc := wire.NewRandomAccount(pkgtest.Prng(t)) + acc := ethwire.NewRandomAccount(pkgtest.Prng(t)) sig, err := acc.Sign(dataToSign) assert.NoError(t, err, "Sign with new account should succeed") assert.NotNil(t, sig) - assert.Equal(t, len(sig), wire.SigLen, "Ethereum signature has wrong length") + assert.Equal(t, len(sig), ethwire.SigLen, "Ethereum signature has wrong length") err = acc.Address().Verify(dataToSign, sig) assert.NoError(t, err, "Verification should succeed") } func TestSignatures_ModifyData_Failure(t *testing.T) { - acc := wire.NewRandomAccount(pkgtest.Prng(t)) + acc := ethwire.NewRandomAccount(pkgtest.Prng(t)) sig, err := acc.Sign(dataToSign) assert.NoError(t, err, "Sign with new account should succeed") assert.NotNil(t, sig) @@ -61,7 +61,7 @@ func TestSignatures_ModifyData_Failure(t *testing.T) { } func TestSignatures_ModifySignature_Failure(t *testing.T) { - acc := wire.NewRandomAccount(pkgtest.Prng(t)) + acc := ethwire.NewRandomAccount(pkgtest.Prng(t)) sig, err := acc.Sign(dataToSign) assert.NoError(t, err, "Sign with new account should succeed") assert.NotNil(t, sig) @@ -76,7 +76,7 @@ func TestSignatures_ModifySignature_Failure(t *testing.T) { } func TestSignatures_ModifyLastByteOfSignature_Failure(t *testing.T) { - acc := wire.NewRandomAccount(pkgtest.Prng(t)) + acc := ethwire.NewRandomAccount(pkgtest.Prng(t)) sig, err := acc.Sign(dataToSign) assert.NoError(t, err, "Sign with new account should succeed") assert.NotNil(t, sig) @@ -91,13 +91,15 @@ func TestSignatures_ModifyLastByteOfSignature_Failure(t *testing.T) { } func TestSignatures_WrongAccount_Failure(t *testing.T) { - acc := wire.NewRandomAccount(pkgtest.Prng(t)) + accPrng := pkgtest.Prng(t) + acc := ethwire.NewRandomAccount(accPrng) sig, err := acc.Sign(dataToSign) assert.NoError(t, err, "Sign with new account should succeed") assert.NotNil(t, sig) // Verify with a wrong account - wrongAcc := wire.NewRandomAccount(pkgtest.Prng(t)) + wrongAcc := ethwire.NewRandomAccount(accPrng) + assert.False(t, acc.Address().Equal(wrongAcc.Address()), "Accounts should be different") err = wrongAcc.Address().Verify(dataToSign, sig) assert.Error(t, err, "Verification should fail with wrong account") }