diff --git a/curve/ecgfp5/point.go b/curve/ecgfp5/point.go index d01efbf..9cfcf92 100644 --- a/curve/ecgfp5/point.go +++ b/curve/ecgfp5/point.go @@ -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 @@ -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: @@ -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 diff --git a/curve/ecgfp5/scalar_field.go b/curve/ecgfp5/scalar_field.go index 81e9635..b14b29a 100644 --- a/curve/ecgfp5/scalar_field.go +++ b/curve/ecgfp5/scalar_field.go @@ -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 @@ -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} @@ -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 } @@ -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 } @@ -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)") + } + var r ECgFp5Scalar for i := 0; i < 5; i++ { // Iteration i computes r <- (r + self*rhs_i + f*n)/2^64. diff --git a/curve/ecgfp5/scalar_field_test.go b/curve/ecgfp5/scalar_field_test.go index 885715c..38d99ea 100644 --- a/curve/ecgfp5/scalar_field_test.go +++ b/curve/ecgfp5/scalar_field_test.go @@ -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") } } @@ -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) + } + }) +} diff --git a/signature/schnorr/schnorr.go b/signature/schnorr/schnorr.go index 910522d..25e463c 100644 --- a/signature/schnorr/schnorr.go +++ b/signature/schnorr/schnorr.go @@ -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 ( @@ -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:]), @@ -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)), @@ -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)), @@ -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