Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filippo version of Ed25519 #462

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
*.o
*.pyc
*.swp
*.deb
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved
*~
moc_*
ext/
.idea/
dissent
docs/html
test.log
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module go.dedis.ch/kyber/v3

require (
filippo.io/edwards25519 v1.0.0-rc.1
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved
github.com/stretchr/testify v1.3.0
go.dedis.ch/fixbuf v1.0.3
go.dedis.ch/protobuf v1.0.11
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
21 changes: 21 additions & 0 deletions group/edwards25519/curve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,24 @@ func BenchmarkPointBaseMul(b *testing.B) { groupBench.PointBaseMul(b.N) }
func BenchmarkPointPick(b *testing.B) { groupBench.PointPick(b.N) }
func BenchmarkPointEncode(b *testing.B) { groupBench.PointEncode(b.N) }
func BenchmarkPointDecode(b *testing.B) { groupBench.PointDecode(b.N) }

//func TestScalarMulAdd(t *testing.T) {
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved
// p1 := new(scalar)
// err1 := p1.UnmarshalBinary([]byte{238, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16})
// if err1 != nil {
// fmt.Println(err1)
// t.Fail()
// }
// p2 := new(scalar)
// err2 := p2.UnmarshalBinary([]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
// if err2 != nil {
// fmt.Println(err2)
// t.Fail()
// }
// p3 := new(point).Mul(p1, nil)
// p4 := new(point).Mul(p2, nil)
// if p3.Equal(p4) {
// fmt.Println("Two different private keys give the same public key")
// t.Fail()
// }
//}
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved
16 changes: 16 additions & 0 deletions group/filippo_ed25519/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package filippo_ed25519

import (
filippo_ed25519 "filippo.io/edwards25519"
"math/big"
)

var primeOrder, _ = new(big.Int).SetString("7237005577332262213973186563042994240857116359379907606001950938285454250989", 10)
var cofactor = new(big.Int).SetInt64(8)
var fullOrder = new(big.Int).Mul(primeOrder, cofactor)

var filippoPrimeOrderScalar = setBigInt(primeOrder)
var filippoCofactorScalar = setBigInt(cofactor)
var filippoNullPoint = Point{filippo_ed25519.NewIdentityPoint()}

var marshalPointID = [8]byte{'e', 'd', '.', 'p', 'o', 'i', 'n', 't'}
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved
75 changes: 75 additions & 0 deletions group/filippo_ed25519/curve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package filippo_ed25519

import (
"crypto/cipher"
"crypto/sha512"
filippo_ed25519 "filippo.io/edwards25519"
"go.dedis.ch/kyber/v3"
"go.dedis.ch/kyber/v3/util/random"
)

// Curve represents the Ed25519 group.
// There are no parameters and no initialization is required
// because it supports only this one specific curve.
type Curve struct {
}

// Return the name of the curve, "Ed25519".
func (c *Curve) String() string {
return "Ed25519"
}

// ScalarLen returns 32, the size in bytes of an encoded Scalar
// for the Ed25519 curve.
func (c *Curve) ScalarLen() int {
return 32
}

// Scalar creates a new Scalar for the prime-order subgroup of the Ed25519 curve.
// The scalars in this package implement kyber.Scalar's SetBytes
// method, interpreting the bytes as a little-endian integer, in order to remain
// compatible with other Ed25519 implementations, and with the standard implementation
// of the EdDSA signature.
func (c *Curve) Scalar() kyber.Scalar {
return &Scalar{new(filippo_ed25519.Scalar)}
}

// PointLen returns 32, the size in bytes of an encoded Point on the Ed25519 curve.
func (c *Curve) PointLen() int {
return 32
}

// Point creates a new Point on the Ed25519 curve.
func (c *Curve) Point() kyber.Point {
P := Point{new(filippo_ed25519.Point)}
return &P
}

// NewKeyAndSeedWithInput returns a formatted Ed25519 key (avoid subgroup attack by
// requiring it to be a multiple of 8). It also returns the input and the digest used
Copy link
Contributor

Choose a reason for hiding this comment

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

requiring it to be a multiple of 8 where is this check being done ?
If I understand correctly, lines 53-54 do not seem to guarantee a multiple of 8, counter example: 0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I am correct then zero would also be a valid output for what we are trying to do.

Copy link
Contributor

Choose a reason for hiding this comment

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

@si-co do you agree ?

Copy link

@si-co si-co Mar 13, 2023

Choose a reason for hiding this comment

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

This function seems to have different problems:

  • It is not documented.
  • I don't understand why it uses sha512 when only 32 bytes are needed;.
  • I seems to try to implement clamping, but it is wrong.
  • It uses digest[:32] to initialize the scalar, but returns digest[32:], which is not the digest used to generate the key.

IMHO the best way to implement this function would be to use SetBytesWithClamping with sha256 to hash the buffer, assuming that's what the function is trying to do. If @parinayc20 adds the appropriate documentation we can continue the discussion.

// to generate the key.
func (c *Curve) NewKeyAndSeedWithInput(buffer []byte) (kyber.Scalar, []byte, []byte) {
digest := sha512.Sum512(buffer[:])
digest[0] &= 0xf8
digest[31] &= 0xf

secret := c.Scalar().(*Scalar)
secret.SetBytes(digest[:32])
return secret, buffer, digest[32:]
}

// NewKeyAndSeed returns a formatted Ed25519 key (avoid subgroup attack by requiring
// it to be a multiple of 8). It also returns the seed and the input used to generate
// the key.
func (c *Curve) NewKeyAndSeed(stream cipher.Stream) (kyber.Scalar, []byte, []byte) {
var buffer [32]byte
random.Bytes(buffer[:], stream)
return c.NewKeyAndSeedWithInput(buffer[:])
}

// NewKey returns a formatted Ed25519 key (avoiding subgroup attack by requiring
// it to be a multiple of 8). NewKey implements the kyber/util/key.Generator interface.
func (c *Curve) NewKey(stream cipher.Stream) kyber.Scalar {
secret, _, _ := c.NewKeyAndSeed(stream)
return secret
}
45 changes: 45 additions & 0 deletions group/filippo_ed25519/curve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package filippo_ed25519

import (
"github.com/stretchr/testify/assert"
"go.dedis.ch/kyber/v3/util/test"
"math"
"testing"
)

var tSuite = NewBlakeSHA256FilippoEd25519()
var groupBench = test.NewGroupBench(tSuite)

func TestSuite(t *testing.T) { test.SuiteTest(t, tSuite) }

func TestCurve_NewKey(t *testing.T) {
group := Curve{}
stream := tSuite.RandomStream()

for i := 0.0; i < math.Pow(10, 6); i++ {
s := group.NewKey(stream).(*Scalar)

// little-endian check of a multiple of 8
b, _ := s.MarshalBinary()
assert.Equal(t, uint8(0), b[0]&7)
}
}

func BenchmarkScalarAdd(b *testing.B) { groupBench.ScalarAdd(b.N) }
func BenchmarkScalarSub(b *testing.B) { groupBench.ScalarSub(b.N) }
func BenchmarkScalarNeg(b *testing.B) { groupBench.ScalarNeg(b.N) }
func BenchmarkScalarMul(b *testing.B) { groupBench.ScalarMul(b.N) }
func BenchmarkScalarDiv(b *testing.B) { groupBench.ScalarDiv(b.N) }
func BenchmarkScalarInv(b *testing.B) { groupBench.ScalarInv(b.N) }
func BenchmarkScalarPick(b *testing.B) { groupBench.ScalarPick(b.N) }
func BenchmarkScalarEncode(b *testing.B) { groupBench.ScalarEncode(b.N) }
func BenchmarkScalarDecode(b *testing.B) { groupBench.ScalarDecode(b.N) }

func BenchmarkPointAdd(b *testing.B) { groupBench.PointAdd(b.N) }
func BenchmarkPointSub(b *testing.B) { groupBench.PointSub(b.N) }
func BenchmarkPointNeg(b *testing.B) { groupBench.PointNeg(b.N) }
func BenchmarkPointMul(b *testing.B) { groupBench.PointMul(b.N) }
func BenchmarkPointBaseMul(b *testing.B) { groupBench.PointBaseMul(b.N) }
func BenchmarkPointPick(b *testing.B) { groupBench.PointPick(b.N) }
func BenchmarkPointEncode(b *testing.B) { groupBench.PointEncode(b.N) }
func BenchmarkPointDecode(b *testing.B) { groupBench.PointDecode(b.N) }
196 changes: 196 additions & 0 deletions group/filippo_ed25519/point.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package filippo_ed25519

import (
"crypto/cipher"
"encoding/hex"
"errors"
filippo_ed25519 "filippo.io/edwards25519"
"go.dedis.ch/kyber/v3"
"io"
)

type Point struct {
point *filippo_ed25519.Point
}

func (p *Point) Equal(s2 kyber.Point) bool {
return p.point.Equal(s2.(*Point).point) == 1
}

func (p *Point) Null() kyber.Point {
p.point = filippo_ed25519.NewIdentityPoint()
return p
}

func (p *Point) Base() kyber.Point {
p.point = filippo_ed25519.NewGeneratorPoint()
return p
}

func (p *Point) Pick(rand cipher.Stream) kyber.Point {
return p.Embed(nil, rand)
}

func (p *Point) Set(a kyber.Point) kyber.Point {
if p.point == nil {
p.point = new(filippo_ed25519.Point)
}
p.point.Set(a.(*Point).point)
return p
}

func (p *Point) Clone() kyber.Point {
p2 := new(Point)
p2.point = new(filippo_ed25519.Point)
p2.point.Set(p.point)
return p2
}
func (p *Point) Add(a, b kyber.Point) kyber.Point {
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved
if p.point == nil {
p.point = new(filippo_ed25519.Point)
}
p.point.Add(a.(*Point).point, b.(*Point).point)
return p
}

func (p *Point) Sub(a, b kyber.Point) kyber.Point {
if p.point == nil {
p.point = new(filippo_ed25519.Point)
}
p.point.Subtract(a.(*Point).point, b.(*Point).point)
return p
}

func (p *Point) Neg(a kyber.Point) kyber.Point {
if p.point == nil {
p.point = new(filippo_ed25519.Point)
}
p.point.Negate(a.(*Point).point)
return p
}

func (p *Point) Mul(a kyber.Scalar, b kyber.Point) kyber.Point {
if p == nil || p.point == nil {
p.point = new(filippo_ed25519.Point)
}
if b == nil || b.(*Point).point == nil {
p.point = p.point.ScalarBaseMult(a.(*Scalar).scalar)
} else {
p.point.ScalarMult(a.(*Scalar).scalar, b.(*Point).point)
}
return p
}

func (p *Point) EmbedLen() int {
// Reserve the most-significant 8 bits for pseudo-randomness.
// Reserve the least-significant 8 bits for embedded data length.
// (Hopefully it's unlikely we'll need >=2048-bit curves soon.)
return (255 - 8 - 8) / 8
}

func (p *Point) Embed(data []byte, rand cipher.Stream) kyber.Point {

// How many bytes to embed?
dl := p.EmbedLen()
if dl > len(data) {
dl = len(data)
}

p.point = new(filippo_ed25519.Point)

for {
// Pick a random point, with optional embedded data
var b [32]byte
rand.XORKeyStream(b[:], b[:])
if data != nil {
b[0] = byte(dl) // Encode length in low 8 bits
copy(b[1:1+dl], data) // Copy in data to embed
}

_, err := p.point.SetBytes(b[:])
if err != nil {
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

it seems this would produce an infinite loop with a maliciously crafted data input, as:

  • SetBytes returns an error if b[:] does not represent a valid point on the curve
  • b[:] is crafted by copying bytes from data and data's length (or a constant, p.EmbedLen()).
  • on failure, we repeat the same deterministic operations

}

if data == nil {
p.Mul(filippoCofactorScalar, p)
if p.Equal(&filippoNullPoint) {
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume there's an expectation here that we're just "unlucky", could you document that?
Also, should the for loop provide guarantees that we're going to eventually come out of this loop ? (e.g. throw some kind of error if we've looped over N times)

}
return p
}
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved

// Since we need the point's y-coordinate to hold our data,
// we must simply check if the point is in the subgroup
// and retry point generation until it is.
var Q Point
Q.Mul(filippoPrimeOrderScalar, p)
if Q.Equal(&filippoNullPoint) {
return p // success
}
// setCannonicalBytes()
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved
// Keep trying...
}
}

func (p *Point) Data() ([]byte, error) {
if p.point == nil {
return nil, errors.New("point not initialized")
}

b := p.point.Bytes()
dl := int(b[0])
if dl > p.EmbedLen() {
return nil, errors.New("invalid embedded data length")
}
return b[1 : 1+dl], nil
}

func (p *Point) MarshalSize() int {
return 32
}

func (p *Point) String() string {
b := p.point.Bytes()
return hex.EncodeToString(b)
}

func (p *Point) MarshalBinary() ([]byte, error) {
if p.point == nil {
return nil, errors.New("point not initialized")
}
return p.point.Bytes(), nil
}

func (p *Point) MarshalID() []byte {
return marshalPointID[:]
}

func (p *Point) UnmarshalBinary(b []byte) error {
if p.point == nil {
p.point = new(filippo_ed25519.Point)
}
_, err := p.point.SetBytes(b)
return err
}

func (p *Point) MarshalTo(w io.Writer) (int, error) {
buf, err := p.MarshalBinary()
if err != nil {
return 0, err
}
return w.Write(buf)
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved
}

func (p *Point) UnmarshalFrom(r io.Reader) (int, error) {
if strm, ok := r.(cipher.Stream); ok {
p.Pick(strm)
return -1, nil // no byte-count when picking randomly
}
buf := make([]byte, p.MarshalSize())
n, err := io.ReadFull(r, buf)
if err != nil {
return n, err
}
return n, p.UnmarshalBinary(buf)
parinayc20 marked this conversation as resolved.
Show resolved Hide resolved
}
Loading