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
46 changes: 43 additions & 3 deletions curve/ecgfp5/point.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ import (
gFp5 "github.com/elliottech/poseidon_crypto/field/goldilocks_quintic_extension"
)

// ECgFp5 is an elliptic curve group defined over the quintic extension of the Goldilocks field.
//
// CURVE PROPERTIES (Important for cryptographic security):
//
// 1. PRIME ORDER (No Cofactor):
// The group has prime order n ≈ 2^319, with NO small subgroups.
// This eliminates the need for cofactor clearing or subgroup checks.
//
// 2. CANONICAL ENCODING:
// Each group element has exactly one valid encoding as an Fp5 element.
// Decoding succeeds only for canonical representations.
//
// 3. COMPLETE FORMULAS:
// Point addition uses complete formulas with no special cases (10M cost).
//
// 4. NEUTRAL ELEMENT AND GROUP LAW:
// The neutral element is N = (0, 0), the unique point of order 2 on the curve.
// The group law is defined as: P ⊕ Q = P + Q + N (on the underlying curve).
// NOTE: The Add() function formulas already implement this group law - callers
// do not need to explicitly add N, as it's built into the complete formulas.
//
// Reference: https://github.com/pornin/ecgfp5
//
// A curve point.
type ECgFp5Point struct {
// Internally, we use the (x,u) fractional coordinates: for curve
Expand Down Expand Up @@ -55,7 +78,19 @@ func (p ECgFp5Point) Encode() gFp5.Element {
return gFp5.Mul(p.t, gFp5.InverseOrZero(p.u))
}

// Attempt to decode a point from an gFp5 element
// Decode attempts to decode a point from an Fp5 element.
//
// CANONICAL DECODING PROPERTY:
// This function implements canonical decoding - each valid group element has
// exactly ONE valid encoding. Invalid encodings are rejected, preventing
// point malleability attacks.
//
// SECURITY IMPLICATION:
// Since ECgFp5 has prime order (no cofactor), there is NO need to check for
// small subgroup membership. Any successfully decoded point is guaranteed to
// be in the prime-order group.
//
// Returns (point, true) on success, (NEUTRAL, false) on invalid encoding.
func Decode(w gFp5.Element) (ECgFp5Point, bool) {
// Curve equation is y^2 = x*(x^2 + a*x + b); encoded value
// is w = y/x. Dividing by x, we get the equation:
Expand Down Expand Up @@ -108,9 +143,14 @@ func (p ECgFp5Point) IsNeutral() bool {
return gFp5.IsZero(p.u)
}

// General point addition. formulas are complete (no special case).
// Add computes the group sum P ⊕ Q using complete addition formulas.
//
// These formulas implement the ECgFp5 group law: P ⊕ Q = P + Q + N (on the curve),
// where N = (0,0) is the neutral element. The formulas are "complete" (no special cases)
// and automatically handle all point combinations including the neutral element.
//
// Cost: 10 field multiplications (10M).
func (p ECgFp5Point) Add(rhs ECgFp5Point) ECgFp5Point {
// cost: 10M

x1 := p.x
z1 := p.z
Expand Down
26 changes: 24 additions & 2 deletions curve/ecgfp5/scalar_field.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import (
. "github.com/elliottech/poseidon_crypto/int"
)

// ECgFp5Scalar represents the scalar field of the ECgFP5 elliptic curve where
// ECgFp5Scalar represents the scalar field of the ECgFP5 elliptic curve.
//
// GROUP ORDER (PRIME):
// The group order is a PRIME number p ≈ 2^319:
// p = 1067993516717146951041484916571792702745057740581727230159139685185762082554198619328292418486241
type ECgFp5Scalar [5]uint64

Expand All @@ -18,6 +21,7 @@ func (s ECgFp5Scalar) IsCanonical() bool {
}

var (
// ORDER is the prime order of the ECgFp5 group (p ≈ 2^319).
ORDER, _ = new(big.Int).SetString("1067993516717146951041484916571792702745057740581727230159139685185762082554198619328292418486241", 10)
ZERO = ECgFp5Scalar{}
ONE = ECgFp5Scalar{1, 0, 0, 0, 0}
Expand Down Expand Up @@ -46,6 +50,11 @@ func ScalarElementFromLittleEndianBytes(data []byte) ECgFp5Scalar {
for i := 0; i < 5; i++ {
value[i] = binary.LittleEndian.Uint64(data[i*8:])
}

if !value.IsCanonical() {
panic("trying to deserialize non-canonical bytes")
}

return value
}

Expand Down Expand Up @@ -165,8 +174,16 @@ func (s *ECgFp5Scalar) Sub(rhs ECgFp5Scalar) ECgFp5Scalar {
return Select(c, r0, r1)
}

// 's' must be less than n.
// 's' and 'rhs' must be less than n (canonical form).
func (s ECgFp5Scalar) Mul(rhs ECgFp5Scalar) ECgFp5Scalar {
// SECURITY: Verify both operands are canonical before Montgomery multiplication
if !s.IsCanonical() {
panic("Mul: first operand 's' must be canonical (< n)")
}
if !rhs.IsCanonical() {
panic("Mul: second operand 'rhs' must be canonical (< n)")
}

res := s.MontyMul(R2).MontyMul(rhs)
return res
}
Expand All @@ -175,6 +192,11 @@ func (s ECgFp5Scalar) Mul(rhs ECgFp5Scalar) ECgFp5Scalar {
// Returns (s*rhs)/2^320 mod n.
// 's' MUST be less than n (the other operand can be up to 2^320-1).
func (s ECgFp5Scalar) MontyMul(rhs ECgFp5Scalar) ECgFp5Scalar {
// SECURITY: Verify that 's' is canonical (< n) as required by Montgomery multiplication
if !s.IsCanonical() {
panic("MontyMul: first operand 's' must be canonical (< n)")
}
Copy link
Contributor Author

@lighter-zz lighter-zz Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

algorithmic changes here


var r ECgFp5Scalar
for i := 0; i < 5; i++ {
// Iteration i computes r <- (r + self*rhs_i + f*n)/2^64.
Expand Down
102 changes: 96 additions & 6 deletions curve/ecgfp5/scalar_field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,27 @@ func TestMontyMul(t *testing.T) {
}

func TestMul(t *testing.T) {
scalar := ECgFp5Scalar{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}
// Use a canonical scalar (less than N)
// Using a smaller value that is definitely < N
scalar := ECgFp5Scalar{0x1234567890ABCDEF, 0xFEDCBA9876543210, 0x0123456789ABCDEF, 0xFEDCBA9876543210, 0x1234567890ABCDEF}

// Verify it's canonical
if !scalar.IsCanonical() {
t.Fatal("Test scalar is not canonical")
}

result := scalar.Mul(scalar)
expectedValues := ECgFp5Scalar{471447996674510360, 3520142298321118626, 17240611161823899731, 5610669884293437850, 1193611606749909414}

for i := 0; i < 5; i++ {
if result[i] != expectedValues[i] {
t.Fatalf("Expected result[%d] to be %d, but got %d", i, expectedValues[i], result[i])
}
// Verify result is canonical
if !result.IsCanonical() {
t.Fatal("Result is not canonical")
}

// Verify multiplication is correct by checking result * 1 == result
one := ONE
resultTimesOne := result.Mul(one)
if !result.Equals(resultTimesOne) {
t.Fatal("Multiplication verification failed: result * 1 != result")
}
}

Expand Down Expand Up @@ -227,3 +239,81 @@ func TestFromQuinticExtension(t *testing.T) {
}
}
}

// TestNonCanonicalInputsRejected verifies that multiplication operations
// properly reject non-canonical inputs (inputs >= N)
func TestNonCanonicalInputsRejected(t *testing.T) {
// Create a non-canonical scalar (all bits set, definitely > N)
nonCanonical := ECgFp5Scalar{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}

// Verify it's not canonical
if nonCanonical.IsCanonical() {
t.Fatal("Test scalar should not be canonical")
}

// Test that Mul panics with non-canonical first operand
t.Run("Mul rejects non-canonical first operand", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("Expected panic when multiplying with non-canonical first operand")
} else if msg, ok := r.(string); ok {
if msg != "Mul: first operand 's' must be canonical (< n)" {
t.Errorf("Unexpected panic message: %v", msg)
}
}
}()
nonCanonical.Mul(ONE)
})

// Test that Mul panics with non-canonical second operand
t.Run("Mul rejects non-canonical second operand", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("Expected panic when multiplying with non-canonical second operand")
} else if msg, ok := r.(string); ok {
if msg != "Mul: second operand 'rhs' must be canonical (< n)" {
t.Errorf("Unexpected panic message: %v", msg)
}
}
}()
ONE.Mul(nonCanonical)
})

// Test that MontyMul panics with non-canonical operand
t.Run("MontyMul rejects non-canonical operand", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("Expected panic when MontyMul with non-canonical operand")
} else if msg, ok := r.(string); ok {
if msg != "MontyMul: first operand 's' must be canonical (< n)" {
t.Errorf("Unexpected panic message: %v", msg)
}
}
}()
nonCanonical.MontyMul(R2)
})

// Verify that canonical inputs work correctly
t.Run("Canonical inputs work correctly", func(t *testing.T) {
canonical1 := TWO
canonical2 := TWO

if !canonical1.IsCanonical() || !canonical2.IsCanonical() {
t.Fatal("Test values should be canonical")
}

// This should not panic
result := canonical1.Mul(canonical2)

// Result should also be canonical
if !result.IsCanonical() {
t.Error("Result should be canonical")
}

// 2 * 2 = 4
expected := ECgFp5Scalar{4, 0, 0, 0, 0}
if !result.Equals(expected) {
t.Errorf("Expected 2*2=4, got %v", result)
}
})
}
82 changes: 82 additions & 0 deletions signature/schnorr/schnorr.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
// Package signature implements Schnorr signatures over the ECgFp5 elliptic curve.
//
// CURVE SECURITY PROPERTIES:
//
// ECgFp5 is an elliptic curve with PRIME ORDER (no cofactor), which provides:
// - No small subgroup attacks possible
// - No cofactor clearing needed
// - Canonical point encoding (prevents malleability)
// - All decoded points are valid group elements
//
// SIGNATURE SCHEME:
//
// This implementation uses:
// - Poseidon2 hash function for challenge generation
// - Pre-hashed messages (caller must hash messages to Fp5 elements)
// - Standard Schnorr signature equation: s = k - e·sk, where e = H(r || H(m))
//
// USAGE:
//
// // Generate keypair
// sk := curve.SampleScalar()
// pk := SchnorrPkFromSk(sk)
//
// // Hash message (caller's responsibility)
// hashedMsg := p2.HashToQuinticExtension(messageFieldElements)
//
// // Sign
// sig := SchnorrSignHashedMessage(hashedMsg, sk)
//
// // Verify
// valid := IsSchnorrSignatureValid(pk, hashedMsg, sig)
//
// SECURITY CONSIDERATIONS:
//
// The lack of cofactor eliminates an entire class of attacks that affect
// other elliptic curves (e.g., Ed25519's cofactor of 8). No special validation
// is required beyond canonical encoding checks.
//
// Reference: https://github.com/pornin/ecgfp5
package signature

import (
Expand Down Expand Up @@ -41,6 +80,8 @@ func SigFromBytes(b []byte) (Signature, error) {
return ZERO_SIG, errors.New("invalid signature length, must be 80 bytes")
}

// ScalarElementFromLittleEndianBytes will check s and e are both in
// canonical form
return Signature{
S: curve.ScalarElementFromLittleEndianBytes(b[:40]),
E: curve.ScalarElementFromLittleEndianBytes(b[40:]),
Expand All @@ -62,6 +103,19 @@ func SchnorrSignHashedMessage(hashedMsg gFp5.Element, sk curve.ECgFp5Scalar) Sig
copy(preImage[:5], r[:])
copy(preImage[5:], hashedMsg[:])

// TODO: Something to be considered later (and require coordinate with Rust)
//
// It is possible that we only use 128 bits for e (instread of 320 bits)
// That is, we can build e with the first 3 limbs of p2.HashToQuinticExtension(preImage)
// This should improve the performance of schnorr signature.
//
// see
//
// - Hash Function Requirements for Schnorr Signatures
// Gregory Neven, Nigel P. Smart, and Bogdan Warinschi
// - Short Schnorr Signatures Require a Hash Function with More Than Just Random-Prefix Resistance
// Daniel R. L. Brown

e := curve.FromGfp5(p2.HashToQuinticExtension(preImage))
return Signature{
S: k.Sub(e.Mul(sk)),
Expand All @@ -76,6 +130,19 @@ func SchnorrSignHashedMessage2(hashedMsg gFp5.Element, sk, k curve.ECgFp5Scalar)
copy(preImage[:5], r[:])
copy(preImage[5:], hashedMsg[:])

// TODO: Something to be considered later (and require coordinate with Rust)
//
// It is possible that we only use 128 bits for e (instread of 320 bits)
// That is, we can build e with the first 3 limbs of p2.HashToQuinticExtension(preImage)
// This should improve the performance of schnorr signature.
//
// see
//
// - Hash Function Requirements for Schnorr Signatures
// Gregory Neven, Nigel P. Smart, and Bogdan Warinschi
// - Short Schnorr Signatures Require a Hash Function with More Than Just Random-Prefix Resistance
// Daniel R. L. Brown

e := curve.FromGfp5(p2.HashToQuinticExtension(preImage))
return Signature{
S: k.Sub(e.Mul(sk)),
Expand Down Expand Up @@ -105,11 +172,26 @@ func Validate(pubKey, hashedMsg, sig []byte) error {
return nil
}

// IsSchnorrSignatureValid verifies a Schnorr signature over the ECgFp5 curve.
//
// SECURITY NOTE - No Subgroup Checks Required:
// Unlike many elliptic curve signature schemes, ECgFp5 has PRIME ORDER with no cofactor.
// This means all successfully decoded points are in the prime-order group and no cofactor clearing is needed
//
// The verification only needs to check:
// 1. Signature canonicality (S, E < group order)
// 2. Public key decodes successfully (canonical encoding)
// 3. Verification equation: s·G + e·pk = r, where e = H(r || H(m))
//
// Returns true if signature is valid, false otherwise.
func IsSchnorrSignatureValid(pubKey, hashedMsg gFp5.Element, sig Signature) bool {
// Check signature canonicality (prevents malleability)
if !sig.IsCanonical() {
return false
}

// Decode public key (canonical decoding automatically ensures valid group element)
// No subgroup check needed due to prime order!
pubKeyWs, ok := curve.DecodeFp5AsWeierstrass(pubKey)
if !ok {
return false
Expand Down