Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions encryption/ecies/publickey.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,20 @@ func PublicKeyFromBytes(b []byte, curve elliptic.Curve) (*PublicKey, error) {
return nil, fmt.Errorf("invalid key length")
}

x := new(big.Int).SetBytes(b[1 : size+1])
y := new(big.Int).SetBytes(b[size+1:])

// Reject points not on the curve to prevent crypto/elliptic.ScalarMult panics
// introduced in Go 1.24+. Without this check, corrupted or malicious payloads
// with valid-length but off-curve coordinates cause a panic in ECDH computation.
// See PAY-2108.
if !curve.IsOnCurve(x, y) {
return nil, fmt.Errorf("point is not on the curve")
}

return &PublicKey{
curve: curve,
Point: &Point{
X: new(big.Int).SetBytes(b[1 : size+1]),
Y: new(big.Int).SetBytes(b[size+1:])},
Point: &Point{X: x, Y: y},
}, nil
}

Expand Down
53 changes: 53 additions & 0 deletions encryption/ecies/publickey_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package ecies_test

import (
"crypto/elliptic"
"encoding/base64"
"encoding/hex"
"math/big"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -61,3 +65,52 @@ func Test_PublicKeyFromHex_Error(t *testing.T) {
assert.Contains(err.Error(), "invalid key length")
assert.Nil(pub)
}

// invalidP521Bytes builds a well-formed uncompressed public key (0x04 || X || Y)
// whose coordinates (1, 1) are NOT on the P521 curve.
// This simulates corrupted or malicious encrypted payloads where the embedded
// ephemeral public key has valid length but invalid curve coordinates.
// Go 1.24+ panics in crypto/elliptic.ScalarMult for such points (PAY-2108).
func invalidP521Bytes() []byte {
size := (elliptic.P521().Params().BitSize + 7) / 8 // 66 bytes
b := make([]byte, 1+2*size)
b[0] = 0x04 // uncompressed point prefix
big.NewInt(1).FillBytes(b[1 : size+1])
big.NewInt(1).FillBytes(b[size+1:])
return b
}

// Test_PublicKeyFromBytes_InvalidCurvePoint verifies that off-curve points are
// rejected at parse time rather than causing a panic in downstream ScalarMult.
func Test_PublicKeyFromBytes_InvalidCurvePoint(t *testing.T) {
assert := assert.New(t)

pub, err := ecies.PublicKeyFromBytes(invalidP521Bytes(), elliptic.P521())
assert.Error(err)
assert.Contains(err.Error(), "point is not on the curve")
assert.Nil(pub)
}

// Test_PublicKeyFromBase64_InvalidCurvePoint ensures the validation propagates
// through the base64 parsing path (PublicKeyFromBase64 -> PublicKeyFromBytes).
func Test_PublicKeyFromBase64_InvalidCurvePoint(t *testing.T) {
assert := assert.New(t)

b64 := base64.StdEncoding.EncodeToString(invalidP521Bytes())
pub, err := ecies.PublicKeyFromBase64(b64, elliptic.P521())
assert.Error(err)
assert.Contains(err.Error(), "point is not on the curve")
assert.Nil(pub)
}

// Test_PublicKeyFromHex_InvalidCurvePoint ensures the validation propagates
// through the hex parsing path (PublicKeyFromHex -> PublicKeyFromBytes).
func Test_PublicKeyFromHex_InvalidCurvePoint(t *testing.T) {
assert := assert.New(t)

h := hex.EncodeToString(invalidP521Bytes())
pub, err := ecies.PublicKeyFromHex(h, elliptic.P521())
assert.Error(err)
assert.Contains(err.Error(), "point is not on the curve")
assert.Nil(pub)
}
Loading