Compare commits

...

13 commits

Author SHA1 Message Date
Felix Lange
95665d5703 version: release go-ethereum v1.16.9 2026-02-17 17:04:51 +01:00
Felix Lange
895a8597cb crypto/secp256k1: fix coordinate check 2026-02-17 17:04:19 +01:00
fengjian
46bee92f9e crypto/ecies: fix ECIES invalid-curve handling (#33669)
Fix ECIES invalid-curve handling in RLPx handshake (reject invalid
ephemeral pubkeys early)
- Add curve validation in crypto/ecies.GenerateShared to reject invalid
public keys before ECDH.
- Update RLPx PoC test to assert invalid curve points fail with
ErrInvalidPublicKey.
 
Motivation / Context
RLPx handshake uses ECIES decryption on unauthenticated network input.
Prior to this change, an invalid-curve ephemeral public key would
proceed into ECDH and only fail at MAC verification, returning
ErrInvalidMessage. This allows an oracle on decrypt success/failure and
leaves the code path vulnerable to invalid-curve/small-subgroup attacks.
The fix enforces IsOnCurve validation up front.
2026-02-17 17:03:47 +01:00
Felix Lange
abeb78c647 Merge branch 'dos-fixes' into release/1.16 2026-01-13 17:04:36 +01:00
Felix Lange
ce43eb98de version: release go-ethereum v1.16.8 stable 2026-01-13 16:29:21 +01:00
lightclient
638741b082 crypto/ecies: use aes blocksize
Co-authored-by: Gary Rong <garyrong0905@gmail.com>
2026-01-13 16:29:21 +01:00
MariusVanDerWijden
fdfd1235ac core/txpool: drop peers on invalid KZG proofs
Co-authored-by: Gary Rong <garyrong0905@gmail.com>
Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>:
2026-01-13 16:29:21 +01:00
Felix Lange
8ecb68623b version: begin v1.16.8 release cycle 2026-01-13 16:19:03 +01:00
Felix Lange
b9f3a3d964 Merge branch 'master' into release/1.16 2025-11-04 13:16:30 +01:00
Felix Lange
386c3de6c4 Merge branch 'master' into release/1.16 2025-11-03 17:47:20 +01:00
Felix Lange
737ffd1bf0 Merge branch 'master' into release/1.16 2025-10-16 09:59:30 +02:00
Felix Lange
41714b4975 Merge branch 'master' into release/1.16 2025-09-25 18:57:39 +02:00
Guillaume Ballet
d818a9af7b version: release v1.16.3 2025-09-01 16:45:45 +02:00
10 changed files with 224 additions and 11 deletions

View file

@ -71,4 +71,7 @@ var (
// ErrInflightTxLimitReached is returned when the maximum number of in-flight
// transactions is reached for specific accounts.
ErrInflightTxLimitReached = errors.New("in-flight transaction limit reached for delegated accounts")
// ErrKZGVerificationError is returned when a KZG proof was not verified correctly.
ErrKZGVerificationError = errors.New("KZG verification error")
)

View file

@ -193,7 +193,7 @@ func validateBlobSidecarLegacy(sidecar *types.BlobTxSidecar, hashes []common.Has
}
for i := range sidecar.Blobs {
if err := kzg4844.VerifyBlobProof(&sidecar.Blobs[i], sidecar.Commitments[i], sidecar.Proofs[i]); err != nil {
return fmt.Errorf("invalid blob %d: %v", i, err)
return fmt.Errorf("%w: invalid blob proof: %v", ErrKZGVerificationError, err)
}
}
return nil
@ -203,7 +203,10 @@ func validateBlobSidecarOsaka(sidecar *types.BlobTxSidecar, hashes []common.Hash
if len(sidecar.Proofs) != len(hashes)*kzg4844.CellProofsPerBlob {
return fmt.Errorf("invalid number of %d blob proofs expected %d", len(sidecar.Proofs), len(hashes)*kzg4844.CellProofsPerBlob)
}
return kzg4844.VerifyCellProofs(sidecar.Blobs, sidecar.Commitments, sidecar.Proofs)
if err := kzg4844.VerifyCellProofs(sidecar.Blobs, sidecar.Commitments, sidecar.Proofs); err != nil {
return fmt.Errorf("%w: %v", ErrKZGVerificationError, err)
}
return nil
}
// ValidationOptionsWithState define certain differences between stateful transaction

View file

@ -124,6 +124,9 @@ func (prv *PrivateKey) GenerateShared(pub *PublicKey, skLen, macLen int) (sk []b
if prv.PublicKey.Curve != pub.Curve {
return nil, ErrInvalidCurve
}
if pub.X == nil || pub.Y == nil || !pub.Curve.IsOnCurve(pub.X, pub.Y) {
return nil, ErrInvalidPublicKey
}
if skLen+macLen > MaxSharedKeyLength(pub) {
return nil, ErrSharedKeyTooBig
}
@ -290,7 +293,7 @@ func (prv *PrivateKey) Decrypt(c, s1, s2 []byte) (m []byte, err error) {
switch c[0] {
case 2, 3, 4:
rLen = (prv.PublicKey.Curve.Params().BitSize + 7) / 4
if len(c) < (rLen + hLen + 1) {
if len(c) < (rLen + hLen + params.BlockSize) {
return nil, ErrInvalidMessage
}
default:

View file

@ -73,6 +73,10 @@ func (bitCurve *BitCurve) Params() *elliptic.CurveParams {
// IsOnCurve returns true if the given (x,y) lies on the BitCurve.
func (bitCurve *BitCurve) IsOnCurve(x, y *big.Int) bool {
if x.Cmp(bitCurve.P) >= 0 || y.Cmp(bitCurve.P) >= 0 {
return false
}
// y² = x³ + b
y2 := new(big.Int).Mul(y, y) //y²
y2.Mod(y2, bitCurve.P) //y²%P

View file

@ -109,8 +109,10 @@ int secp256k1_ext_scalar_mul(const secp256k1_context* ctx, unsigned char *point,
ARG_CHECK(scalar != NULL);
(void)ctx;
secp256k1_fe_set_b32_limit(&feX, point);
secp256k1_fe_set_b32_limit(&feY, point+32);
if (!secp256k1_fe_set_b32_limit(&feX, point) ||
!secp256k1_fe_set_b32_limit(&feY, point+32)) {
return 0;
}
secp256k1_ge_set_xy(&ge, &feX, &feY);
secp256k1_scalar_set_b32(&s, scalar, &overflow);
if (overflow || secp256k1_scalar_is_zero(&s)) {

View file

@ -164,6 +164,13 @@ type btCurve struct {
*secp256k1.KoblitzCurve
}
func (curve btCurve) IsOnCurve(x, y *big.Int) bool {
if x.Cmp(secp256k1.Params().P) >= 0 || y.Cmp(secp256k1.Params().P) >= 0 {
return false
}
return curve.KoblitzCurve.IsOnCurve(x, y)
}
// Marshal converts a point given as (x, y) into a byte slice.
func (curve btCurve) Marshal(x, y *big.Int) []byte {
byteLen := (curve.Params().BitSize + 7) / 8

View file

@ -114,10 +114,11 @@ type txRequest struct {
// txDelivery is the notification that a batch of transactions have been added
// to the pool and should be untracked.
type txDelivery struct {
origin string // Identifier of the peer originating the notification
hashes []common.Hash // Batch of transaction hashes having been delivered
metas []txMetadata // Batch of metadata associated with the delivered hashes
direct bool // Whether this is a direct reply or a broadcast
origin string // Identifier of the peer originating the notification
hashes []common.Hash // Batch of transaction hashes having been delivered
metas []txMetadata // Batch of metadata associated with the delivered hashes
direct bool // Whether this is a direct reply or a broadcast
violation error // Whether we encountered a protocol violation
}
// txDrop is the notification that a peer has disconnected.
@ -285,6 +286,7 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
knownMeter = txReplyKnownMeter
underpricedMeter = txReplyUnderpricedMeter
otherRejectMeter = txReplyOtherRejectMeter
violation error
)
if !direct {
inMeter = txBroadcastInMeter
@ -331,6 +333,12 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
case errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) || errors.Is(err, txpool.ErrTxGasPriceTooLow):
underpriced++
case errors.Is(err, txpool.ErrKZGVerificationError):
// KZG verification failed, terminate transaction processing immediately.
// Since KZG verification is computationally expensive, this acts as a
// defensive measure against potential DoS attacks.
violation = err
default:
otherreject++
}
@ -339,6 +347,11 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
kind: batch[j].Type(),
size: uint32(batch[j].Size()),
})
// Terminate the transaction processing if violation is encountered. All
// the remaining transactions in response will be silently discarded.
if violation != nil {
break
}
}
knownMeter.Mark(duplicate)
underpricedMeter.Mark(underpriced)
@ -349,9 +362,13 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
time.Sleep(200 * time.Millisecond)
log.Debug("Peer delivering stale transactions", "peer", peer, "rejected", otherreject)
}
// If we encountered a protocol violation, disconnect this peer.
if violation != nil {
break
}
}
select {
case f.cleanup <- &txDelivery{origin: peer, hashes: added, metas: metas, direct: direct}:
case f.cleanup <- &txDelivery{origin: peer, hashes: added, metas: metas, direct: direct, violation: violation}:
return nil
case <-f.quit:
return errTerminated
@ -746,6 +763,11 @@ func (f *TxFetcher) loop() {
// Something was delivered, try to reschedule requests
f.scheduleFetches(timeoutTimer, timeoutTrigger, nil) // Partial delivery may enable others to deliver too
}
// If we encountered a protocol violation, disconnect the peer
if delivery.violation != nil {
log.Warn("Disconnect peer for protocol violation", "peer", delivery.origin, "error", delivery.violation)
f.dropPeer(delivery.origin)
}
case drop := <-f.drop:
// A peer was dropped, remove all traces of it

View file

@ -17,6 +17,7 @@
package fetcher
import (
"crypto/sha256"
"errors"
"math/big"
"math/rand"
@ -28,7 +29,10 @@ import (
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
var (
@ -1908,6 +1912,114 @@ func TestTransactionFetcherDropAlternates(t *testing.T) {
})
}
func makeInvalidBlobTx() *types.Transaction {
key, _ := crypto.GenerateKey()
blob := &kzg4844.Blob{byte(0xa)}
commitment, _ := kzg4844.BlobToCommitment(blob)
blobHash := kzg4844.CalcBlobHashV1(sha256.New(), &commitment)
cellProof, _ := kzg4844.ComputeCellProofs(blob)
// Mutate the cell proof
cellProof[0][0] = 0x0
blobtx := &types.BlobTx{
ChainID: uint256.MustFromBig(params.MainnetChainConfig.ChainID),
Nonce: 0,
GasTipCap: uint256.NewInt(100),
GasFeeCap: uint256.NewInt(200),
Gas: 21000,
BlobFeeCap: uint256.NewInt(200),
BlobHashes: []common.Hash{blobHash},
Value: uint256.NewInt(100),
Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion1, []kzg4844.Blob{*blob}, []kzg4844.Commitment{commitment}, cellProof),
}
return types.MustSignNewTx(key, types.LatestSigner(params.MainnetChainConfig), blobtx)
}
// This test ensures that the peer will be disconnected for protocol violation
// and all its internal traces should be removed properly.
func TestTransactionProtocolViolation(t *testing.T) {
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
var (
badTx = makeInvalidBlobTx()
drop = make(chan struct{}, 1)
)
testTransactionFetcherParallel(t, txFetcherTest{
init: func() *TxFetcher {
return NewTxFetcher(
func(common.Hash) bool { return false },
func(txs []*types.Transaction) []error {
var errs []error
for range txs {
errs = append(errs, txpool.ErrKZGVerificationError)
}
return errs
},
func(a string, b []common.Hash) error {
return nil
},
func(peer string) { drop <- struct{}{} },
)
},
steps: []interface{}{
// Initial announcement to get something into the waitlist
doTxNotify{
peer: "A",
hashes: []common.Hash{testTxs[0].Hash(), badTx.Hash(), testTxs[1].Hash()},
types: []byte{types.LegacyTxType, types.BlobTxType, types.LegacyTxType},
sizes: []uint32{uint32(testTxs[0].Size()), uint32(badTx.Size()), uint32(testTxs[1].Size())},
},
isWaiting(map[string][]announce{
"A": {
{testTxs[0].Hash(), types.LegacyTxType, uint32(testTxs[0].Size())},
{badTx.Hash(), types.BlobTxType, uint32(badTx.Size())},
{testTxs[1].Hash(), types.LegacyTxType, uint32(testTxs[1].Size())},
},
}),
doWait{time: 0, step: true}, // zero time, but the blob fetching should be scheduled
isWaiting(map[string][]announce{
"A": {
{testTxs[0].Hash(), types.LegacyTxType, uint32(testTxs[0].Size())},
{testTxs[1].Hash(), types.LegacyTxType, uint32(testTxs[1].Size())},
},
}),
isScheduled{
tracking: map[string][]announce{
"A": {
{badTx.Hash(), types.BlobTxType, uint32(badTx.Size())},
},
},
fetching: map[string][]common.Hash{
"A": {badTx.Hash()},
},
},
doTxEnqueue{
peer: "A",
txs: []*types.Transaction{badTx},
direct: true,
},
// Some internal traces are left and will be cleaned by a following drop
// operation.
isWaiting(map[string][]announce{
"A": {
{testTxs[0].Hash(), types.LegacyTxType, uint32(testTxs[0].Size())},
{testTxs[1].Hash(), types.LegacyTxType, uint32(testTxs[1].Size())},
},
}),
isScheduled{},
doFunc(func() { <-drop }),
// Simulate the drop operation emitted by the server
doDrop("A"),
isWaiting(nil),
isScheduled{nil, nil, nil},
},
})
}
func testTransactionFetcherParallel(t *testing.T, tt txFetcherTest) {
t.Parallel()
testTransactionFetcher(t, tt)

View file

@ -0,0 +1,57 @@
package rlpx
import (
"bytes"
"errors"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
)
func TestHandshakeECIESInvalidCurveOracle(t *testing.T) {
initKey, err := crypto.GenerateKey()
if err != nil {
t.Fatal(err)
}
respKey, err := crypto.GenerateKey()
if err != nil {
t.Fatal(err)
}
init := handshakeState{
initiator: true,
remote: ecies.ImportECDSAPublic(&respKey.PublicKey),
}
authMsg, err := init.makeAuthMsg(initKey)
if err != nil {
t.Fatal(err)
}
packet, err := init.sealEIP8(authMsg)
if err != nil {
t.Fatal(err)
}
var recv handshakeState
if _, err := recv.readMsg(new(authMsgV4), respKey, bytes.NewReader(packet)); err != nil {
t.Fatalf("expected valid packet to decrypt: %v", err)
}
tampered := append([]byte(nil), packet...)
if len(tampered) < 2+65 {
t.Fatalf("unexpected packet length %d", len(tampered))
}
tampered[2] = 0x04
for i := 1; i < 65; i++ {
tampered[2+i] = 0x00
}
var recv2 handshakeState
_, err = recv2.readMsg(new(authMsgV4), respKey, bytes.NewReader(tampered))
if err == nil {
t.Fatal("expected decryption failure for invalid curve point")
}
if !errors.Is(err, ecies.ErrInvalidPublicKey) {
t.Fatalf("unexpected error: %v", err)
}
}

View file

@ -19,6 +19,6 @@ package version
const (
Major = 1 // Major version component of the current release
Minor = 16 // Minor version component of the current release
Patch = 7 // Patch version component of the current release
Patch = 9 // Patch version component of the current release
Meta = "stable" // Version metadata to append to the version string
)