diff --git a/go.mod b/go.mod index 6b0d8b2b..cd090d54 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/cloudflare/circl go 1.12 require ( + github.com/armfazh/h2c-go-ref v0.0.0-20210215173008-07e12a6f8e0d + github.com/bwesterb/go-ristretto v1.1.1 + github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 golang.org/x/sys v0.0.0-20201211090839-8ad439b19e0f ) diff --git a/go.sum b/go.sum index 47bc7b9e..8a93c011 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,18 @@ +github.com/armfazh/h2c-go-ref v0.0.0-20210215173008-07e12a6f8e0d h1:0YBiQ+rXap26i6apP7AldB+h50fAP7XSzpbio4ElXQo= +github.com/armfazh/h2c-go-ref v0.0.0-20210215173008-07e12a6f8e0d/go.mod h1:8fwPDRbWg9lh+s+iVv/7yAthCGHoGLTpeXnHf/J5EXs= +github.com/armfazh/tozan-ecc v0.1.3 h1:g3OKE0KR4L/GZaoQYzsOUdg97Ey5lZRl1i1fD/QkUUw= +github.com/armfazh/tozan-ecc v0.1.3/go.mod h1:u25eZC5Z8uJFQxJxGBz1Blfii/7m3DfmwX0vFnwtG9I= +github.com/bwesterb/go-ristretto v1.1.1 h1:ScMQxfIReRWsrKhQ+rR9R4CoaS+9Mf+GqaGP8NQEEJg= +github.com/bwesterb/go-ristretto v1.1.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +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= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604= golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -8,3 +22,6 @@ golang.org/x/sys v0.0.0-20201211090839-8ad439b19e0f h1:QdHQnPce6K4XQewki9WNbG5KO golang.org/x/sys v0.0.0-20201211090839-8ad439b19e0f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/group/group_test.go b/group/group_test.go index b79533fd..2eaa0380 100644 --- a/group/group_test.go +++ b/group/group_test.go @@ -16,6 +16,7 @@ func TestGroup(t *testing.T) { group.P256, group.P384, group.P521, + group.Ristretto255, } { g := g n := g.(fmt.Stringer).String() @@ -113,16 +114,24 @@ func testOrder(t *testing.T, testTimes int, g group.Group) { } } +func isZero(b []byte) bool { + for i := 0; i < len(b); i++ { + if b[i] != 0x00 { + return false + } + } + return true +} + func testMarshal(t *testing.T, testTimes int, g group.Group) { I := g.Identity() got, _ := I.MarshalBinary() - want := []byte{0} - if !bytes.Equal(got, want) { - test.ReportError(t, got, want) + if !isZero(got) { + test.ReportError(t, got, "Non-zero identity") } got, _ = I.MarshalBinaryCompress() - if !bytes.Equal(got, want) { - test.ReportError(t, got, want) + if !isZero(got) { + test.ReportError(t, got, "Non-zero identity") } II := g.NewElement() err := II.UnmarshalBinary(got) diff --git a/group/ristretto255.go b/group/ristretto255.go new file mode 100644 index 00000000..401bb4af --- /dev/null +++ b/group/ristretto255.go @@ -0,0 +1,187 @@ +package group + +import ( + "crypto" + "io" + + h2c "github.com/armfazh/h2c-go-ref" + + r255 "github.com/bwesterb/go-ristretto" +) + +var ( + Ristretto255 Group = ristrettoGroup{} +) + +type ristrettoGroup struct { +} + +func (g ristrettoGroup) String() string { + return "ristretto255" +} + +type ristrettoElement struct { + p r255.Point +} + +type ristrettoScalar struct { + s r255.Scalar +} + +func (g ristrettoGroup) NewElement() Element { + return g.Identity() +} + +func (g ristrettoGroup) NewScalar() Scalar { + return &ristrettoScalar{ + s: r255.Scalar{}, + } +} + +func (g ristrettoGroup) Identity() Element { + var zero r255.Point + zero.SetZero() + return &ristrettoElement{ + p: zero, + } +} + +func (g ristrettoGroup) Generator() Element { + var base r255.Point + base.SetBase() + return &ristrettoElement{ + p: base, + } +} + +func (g ristrettoGroup) Order() Scalar { + q := r255.Scalar{ + 0x5cf5d3ed, 0x5812631a, 0xa2f79cd6, 0x14def9de, + 0x00000000, 0x00000000, 0x00000000, 0x10000000, + } + return &ristrettoScalar{ + s: q, + } +} + +func (g ristrettoGroup) RandomElement(r io.Reader) Element { + var x r255.Point + x.Rand() + return &ristrettoElement{ + p: x, + } +} + +func (g ristrettoGroup) RandomScalar(r io.Reader) Scalar { + var x r255.Scalar + x.Rand() + return &ristrettoScalar{ + s: x, + } +} + +func (g ristrettoGroup) HashToElement(msg, dst []byte) Element { + e := g.NewElement() + + expID := h2c.ExpanderDesc{ + Type: h2c.XMD, + ID: uint(crypto.SHA512), + } + exp, err := expID.Get(dst, 0) + if err != nil { + panic(err) + } + data := exp.Expand(msg, 64) + + e.(*ristrettoElement).p.Derive(data) + return e +} + +func (g ristrettoGroup) HashToScalar(msg, dst []byte) Scalar { + s := g.NewScalar() + s.(*ristrettoScalar).s.Derive(msg) + return s +} + +func (e *ristrettoElement) IsIdentity() bool { + var zero r255.Point + zero.SetZero() + return e.p.Equals(&zero) +} + +func (e *ristrettoElement) IsEqual(x Element) bool { + return e.p.Equals(&x.(*ristrettoElement).p) +} + +func (e *ristrettoElement) Add(x Element, y Element) Element { + e.p.Add(&x.(*ristrettoElement).p, &y.(*ristrettoElement).p) + return e +} + +func (e *ristrettoElement) Dbl(x Element) Element { + return e.Add(x, x) +} + +func (e *ristrettoElement) Neg(x Element) Element { + e.p.Neg(&x.(*ristrettoElement).p) + return e +} + +func (e *ristrettoElement) Mul(x Element, y Scalar) Element { + e.p.ScalarMult(&x.(*ristrettoElement).p, &y.(*ristrettoScalar).s) + return e +} + +func (e *ristrettoElement) MulGen(x Scalar) Element { + e.p.ScalarMultBase(&x.(*ristrettoScalar).s) + return e +} + +func (e *ristrettoElement) MarshalBinaryCompress() ([]byte, error) { + return e.p.MarshalBinary() +} + +func (e *ristrettoElement) MarshalBinary() ([]byte, error) { + return e.p.MarshalBinary() +} + +func (e *ristrettoElement) UnmarshalBinary(data []byte) error { + return e.p.UnmarshalBinary(data) +} + +func (s *ristrettoScalar) IsEqual(x Scalar) bool { + return s.s.Equals(&x.(*ristrettoScalar).s) +} + +func (s *ristrettoScalar) Add(x Scalar, y Scalar) Scalar { + s.s.Add(&x.(*ristrettoScalar).s, &y.(*ristrettoScalar).s) + return s +} + +func (s *ristrettoScalar) Sub(x Scalar, y Scalar) Scalar { + s.s.Sub(&x.(*ristrettoScalar).s, &y.(*ristrettoScalar).s) + return s +} + +func (s *ristrettoScalar) Mul(x Scalar, y Scalar) Scalar { + s.s.Mul(&x.(*ristrettoScalar).s, &y.(*ristrettoScalar).s) + return s +} + +func (s *ristrettoScalar) Neg(x Scalar) Scalar { + s.s.Neg(&x.(*ristrettoScalar).s) + return s +} + +func (s *ristrettoScalar) Inv(x Scalar) Scalar { + s.s.Inverse(&x.(*ristrettoScalar).s) + return s +} + +func (s *ristrettoScalar) MarshalBinary() ([]byte, error) { + return s.s.MarshalBinary() +} + +func (s *ristrettoScalar) UnmarshalBinary(data []byte) error { + return s.s.UnmarshalBinary(data) +} diff --git a/group/ristretto255_test.go b/group/ristretto255_test.go new file mode 100644 index 00000000..9f7a3231 --- /dev/null +++ b/group/ristretto255_test.go @@ -0,0 +1,145 @@ +package group + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "testing" +) + +// https://tools.ietf.org/html/draft-irtf-cfrg-ristretto255-decaf448-00#appendix-A.1 +func TestGeneratorMultiples(t *testing.T) { + encVec := []string{ + "0000000000000000000000000000000000000000000000000000000000000000", + "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76", + "6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919", + "94741f5d5d52755ece4f23f044ee27d5d1ea1e2bd196b462166b16152a9d0259", + "da80862773358b466ffadfe0b3293ab3d9fd53c5ea6c955358f568322daf6a57", + "e882b131016b52c1d3337080187cf768423efccbb517bb495ab812c4160ff44e", + "f64746d3c92b13050ed8d80236a7f0007c3b3f962f5ba793d19a601ebb1df403", + "44f53520926ec81fbd5a387845beb7df85a96a24ece18738bdcfa6a7822a176d", + "903293d8f2287ebe10e2374dc1a53e0bc887e592699f02d077d5263cdd55601c", + "02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031", + "20706fd788b2720a1ed2a5dad4952b01f413bcf0e7564de8cdc816689e2db95f", + "bce83f8ba5dd2fa572864c24ba1810f9522bc6004afe95877ac73241cafdab42", + "e4549ee16b9aa03099ca208c67adafcafa4c3f3e4e5303de6026e3ca8ff84460", + "aa52e000df2e16f55fb1032fc33bc42742dad6bd5a8fc0be0167436c5948501f", + "46376b80f409b29dc2b5f6f0c52591990896e5716f41477cd30085ab7f10301e", + "e0c418f7c8d9c4cdd7395b93ea124f3ad99021bb681dfc3302a9d99a2e53e64e", + } + + g := Ristretto255 + base := g.NewElement() + encBase, err := hex.DecodeString(encVec[0]) + if err != nil { + t.Fatal("DecodeString") + } + err = base.UnmarshalBinary(encBase) + if err != nil { + t.Fatal("UnmarshalBinary") + } + if !base.IsIdentity() { + t.Fatal("Base element is not identity") + } + + for i := 1; i < len(encVec); i++ { + base.Add(base, g.Generator()) + baseEnc, err := base.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary %d", i) + } + if hex.EncodeToString(baseEnc) != encVec[i] { + t.Fatalf("Multiple %d mismatch", i) + } + } +} + +// https://tools.ietf.org/html/draft-irtf-cfrg-ristretto255-decaf448-00#appendix-A.2 +func TestInvalidEncodings(t *testing.T) { + encVec := []string{ + // Non-canonical field encodings. + "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + // The internal ristretto255 implementation ignores the MSB, so the following two encodings fail. + // "f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + // "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + // Negative field elements. + "0100000000000000000000000000000000000000000000000000000000000000", + "01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "ed57ffd8c914fb201471d1c3d245ce3c746fcbe63a3679d51b6a516ebebe0e20", + "c34c4e1826e5d403b78e246e88aa051c36ccf0aafebffe137d148a2bf9104562", + "c940e5a4404157cfb1628b108db051a8d439e1a421394ec4ebccb9ec92a8ac78", + "47cfc5497c53dc8e61c91d17fd626ffb1c49e2bca94eed052281b510b1117a24", + "f1c6165d33367351b0da8f6e4511010c68174a03b6581212c71c0e1d026c3c72", + "87260f7a2f12495118360f02c26a470f450dadf34a413d21042b43b9d93e1309", + // Non-square x^2. + "26948d35ca62e643e26a83177332e6b6afeb9d08e4268b650f1f5bbd8d81d371", + "4eac077a713c57b4f4397629a4145982c661f48044dd3f96427d40b147d9742f", + "de6a7b00deadc788eb6b6c8d20c0ae96c2f2019078fa604fee5b87d6e989ad7b", + "bcab477be20861e01e4a0e295284146a510150d9817763caf1a6f4b422d67042", + "2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08", + "f4a9e534fc0d216c44b218fa0c42d99635a0127ee2e53c712f70609649fdff22", + "8268436f8c4126196cf64b3c7ddbda90746a378625f9813dd9b8457077256731", + "2810e5cbc2cc4d4eece54f61c6f69758e289aa7ab440b3cbeaa21995c2f4232b", + // Negative xy value. + "3eb858e78f5a7254d8c9731174a94f76755fd3941c0ac93735c07ba14579630e", + "a45fdc55c76448c049a1ab33f17023edfb2be3581e9c7aade8a6125215e04220", + "d483fe813c6ba647ebbfd3ec41adca1c6130c2beeee9d9bf065c8d151c5f396e", + "8a2e1d30050198c65a54483123960ccc38aef6848e1ec8f5f780e8523769ba32", + "32888462f8b486c68ad7dd9610be5192bbeaf3b443951ac1a8118419d9fa097b", + "227142501b9d4355ccba290404bde41575b037693cef1f438c47f8fbf35d1165", + "5c37cc491da847cfeb9281d407efc41e15144c876e0170b499a96a22ed31e01e", + "445425117cb8c90edcbc7c1cc0e74f747f2c1efa5630a967c64f287792a48a4b", + // s = -1, which causes y = 0. + "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + } + + for i, enc := range encVec { + raw, err := hex.DecodeString(enc) + if err != nil { + t.Fatal("DecodeString") + } + err = Ristretto255.NewElement().UnmarshalBinary(raw) + if err == nil { + t.Fatalf("Decode succeeded for vector %d: %v", i, enc) + } + } +} + +func TestRistrettoElGamal(t *testing.T) { + g := Ristretto255 + pk := g.NewElement() + sk := g.RandomScalar(rand.Reader) + pk.MulGen(sk) + + // El'Gamal encrypt a random element p into a ciphertext-pair (c1,c2) + p := g.RandomElement(rand.Reader) + r := g.RandomScalar(rand.Reader) + c2 := g.NewElement() + c1 := g.NewElement() + c2.MulGen(r) + c1.Mul(pk, r) + c1.Add(c1, p) + + // Decrypt (c1,c2) back to p + b := g.NewElement() + p2 := g.NewElement() + b.Mul(c2, sk) + p2.Add(c1, b.Neg(b)) + + if !p.IsEqual(p2) { + t.Fatal("Encryption/decryption failed") + } + + pEnc, err := p.MarshalBinary() + if err != nil { + t.Fatal("MarshalBinary") + } + p2Enc, err := p2.MarshalBinary() + if err != nil { + t.Fatal("MarshalBinary") + } + if !bytes.Equal(pEnc, p2Enc) { + t.Fatal("Unequal encodings") + } +}