Skip to content

Commit

Permalink
feat(crypto): supporting ed25519 (#1481)
Browse files Browse the repository at this point in the history
  • Loading branch information
kehiy authored Aug 22, 2024
1 parent b7e0039 commit 4eb5335
Show file tree
Hide file tree
Showing 20 changed files with 1,780 additions and 18 deletions.
3 changes: 2 additions & 1 deletion crypto/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const (
)

const (
SignatureTypeBLS byte = 1
SignatureTypeBLS byte = 1
SignatureTypeEd25519 byte = 3
)

const (
Expand Down
3 changes: 3 additions & 0 deletions crypto/bls/hdkeychain/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ var (
// ErrInvalidKeyData describes an error in which the provided key is
// not valid.
ErrInvalidKeyData = errors.New("key data is invalid")

// ErrInvalidHRP describes an error in which the HRP is not valid.
ErrInvalidHRP = errors.New("HRP is invalid")
)
10 changes: 7 additions & 3 deletions crypto/bls/hdkeychain/extendedkey.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package hdkeychain

// References:
// [PIP-11]: Deterministic key hierarchy for BLS12-381 curve
// PIP-11: Deterministic key hierarchy for BLS12-381 curve
// https://pips.pactus.org/PIPs/pip-11

import (
Expand Down Expand Up @@ -448,9 +448,13 @@ func NewKeyFromString(str string) (*ExtendedKey, error) {
return nil, err
}

isPrivate := true
if hrp == crypto.XPublicKeyHRP {
var isPrivate bool
if hrp == crypto.XPrivateKeyHRP {
isPrivate = true
} else if hrp == crypto.XPublicKeyHRP {
isPrivate = false
} else {
return nil, ErrInvalidHRP
}

return newExtendedKey(key, chainCode, path, isPrivate, pubOnG1), nil
Expand Down
88 changes: 80 additions & 8 deletions crypto/bls/hdkeychain/extendedkey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,77 @@ func TestGenerateSeed(t *testing.T) {
seed, err := GenerateSeed(test.length)
assert.ErrorIs(t, err, test.err)

if test.err == nil && len(seed) != int(test.length) {
t.Errorf("GenerateSeed #%d (%s): length mismatch -- "+
"got %d, want %d", i, test.name, len(seed),
test.length)
if test.err == nil {
assert.Len(t, seed, int(test.length),
"GenerateSeed #%d (%s): length mismatch -- got %d, want %d",
i, test.name, len(seed), test.length)
}
}
}

// TestNewMaster ensures the NewMaster function works as intended.
func TestNewMaster(t *testing.T) {
tests := []struct {
name string
seed string
privKey string
err error
}{
// Test various valid seeds.
{
name: "16 bytes",
seed: "000102030405060708090a0b0c0d0e0f",
privKey: "4f55e31ee1c4f58af0840fd3f5e635fd6c07eacd14283c45d7d43729003abb84",
},
{
name: "32 bytes",
seed: "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678",
privKey: "4c101174339ffca5cc0afca5d2d8e2538834781318e5e1c8afdabf7e6fb77444",
},
{
name: "64 bytes",
seed: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7" +
"b7875726f6c696663605d5a5754514e4b484542",
privKey: "47b660cc8dc2d4dc2cdf8893048bda9d5dc6318eb31f301b272b291b26cb20a1",
},

// Test invalid seeds.
{
name: "empty seed",
seed: "",
err: ErrInvalidSeedLen,
},
{
name: "15 bytes",
seed: "000000000000000000000000000000",
err: ErrInvalidSeedLen,
},
{
name: "65 bytes",
seed: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
"000000000000000000000000000000000000000000000",
err: ErrInvalidSeedLen,
},
}

for i, test := range tests {
seed, _ := hex.DecodeString(test.seed)
extKeyG1, err := NewMaster(seed, true)
assert.ErrorIs(t, err, test.err)

extKeyG2, err := NewMaster(seed, true)
assert.ErrorIs(t, err, test.err)

if test.err == nil {
privKeyG1, _ := extKeyG1.RawPrivateKey()
assert.Equal(t, test.privKey, hex.EncodeToString(privKeyG1),
"NewMaster #%d (%s): privKeyG1 mismatch -- got %x, want %s",
i+1, test.name, privKeyG1, test.privKey)

continue
privKeyG2, _ := extKeyG2.RawPrivateKey()
assert.Equal(t, test.privKey, hex.EncodeToString(privKeyG2),
"NewMaster #%d (%s): privKeyG2 mismatch -- got %x, want %s",
i+1, test.name, privKeyG2, test.privKey)
}
}
}
Expand Down Expand Up @@ -411,12 +476,19 @@ func TestInvalidString(t *testing.T) {
expectedError: ErrInvalidKeyData,
},
{
str: "SECRET1ZQ5QQQQYQQYQQQQQZQQQGQQSQQQQQPJ568VS9LZ67JKWW0P6TQY9NY58LV0PCVRQQTAEMKGV6ULJNS99Y68JHCVGPYPZTWSAST8PWFJMJQDU0FU8D4YMF58CZ998PGRN29EZYHLWNDVDDJE7XP6L",
desc: "invalid type",
str: "XPUBLIC1ZQ5QQQQYQQYQQQQQZQQQGQQSQQQQQPJ568VS9LZ67JKWW0P6TQY9NY58LV0PCVRQQTAEMKGV6ULJNS99Y68JHCVGPYPZTWSAST8PWFJMJQDU0FU8D4YMF58CZ998PGRN29EZYHLWNDVDDJ3HALEC",
expectedError: ErrInvalidKeyData,
},
{
str: "XPUBLIC1ZQ5QQQQYQQYQQQQQZQQQGQQSQQQQQPJ568VS9LZ67JKWW0P6TQY9NY58LV0PCVRQQTAEMKGV6ULJNS99Y68JHCVGPYPZTWSAST8PWFJMJQDU0FU8D4YMF58CZ998PGRN29EZYHLWNDVDDJ3HALEC",
expectedError: ErrInvalidKeyData,
desc: "invalid hrp",
str: "SECRET1PQ5QQQQYQQYQQQQQZQQQGQQSQQQQQPJ568VS9LZ67JKWW0P6TQY9NY58LV0PCVRQQTAEMKGV6ULJNS99Y68JHCVGPYPZTWSAST8PWFJMJQDU0FU8D4YMF58CZ998PGRN29EZYHLWNDVDDJ98PYV5",
expectedError: ErrInvalidHRP,
},
{
desc: "invalid hrp",
str: "PUBLIC1PQ5QQQQYQQYQQQQQZQQQGQQSQQQQQPJ568VS9LZ67JKWW0P6TQY9NY58LV0PCVRQQTAEMKGV6ULJNS99Y68JHCVGPYPZTWSAST8PWFJMJQDU0FU8D4YMF58CZ998PGRN29EZYHLWNDVDDJ4Z2HK2",
expectedError: ErrInvalidHRP,
},
}

Expand Down
4 changes: 2 additions & 2 deletions crypto/bls/private_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,6 @@ func (prv *PrivateKey) PublicKey() crypto.PublicKey {
return prv.PublicKeyNative()
}

func (prv *PrivateKey) EqualsTo(right crypto.PrivateKey) bool {
return prv.fr.Equal(&right.(*PrivateKey).fr)
func (prv *PrivateKey) EqualsTo(x crypto.PrivateKey) bool {
return prv.fr.Equal(&x.(*PrivateKey).fr)
}
5 changes: 3 additions & 2 deletions crypto/bls/public_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bls

import (
"bytes"
"crypto/subtle"
"fmt"
"io"

Expand Down Expand Up @@ -139,8 +140,8 @@ func (pub *PublicKey) Verify(msg []byte, sig crypto.Signature) error {
}

// EqualsTo checks if the current public key is equal to another public key.
func (pub *PublicKey) EqualsTo(right crypto.PublicKey) bool {
return bytes.Equal(pub.data, right.(*PublicKey).data)
func (pub *PublicKey) EqualsTo(x crypto.PublicKey) bool {
return subtle.ConstantTimeCompare(pub.data, x.(*PublicKey).data) == 1
}

// AccountAddress returns the account address derived from the public key.
Expand Down
5 changes: 3 additions & 2 deletions crypto/bls/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bls

import (
"bytes"
"crypto/subtle"
"encoding/hex"
"fmt"
"io"
Expand Down Expand Up @@ -88,8 +89,8 @@ func (sig *Signature) Decode(r io.Reader) error {
}

// EqualsTo checks if the current signature is equal to another signature.
func (sig *Signature) EqualsTo(right crypto.Signature) bool {
return bytes.Equal(sig.data, right.(*Signature).data)
func (sig *Signature) EqualsTo(x crypto.Signature) bool {
return subtle.ConstantTimeCompare(sig.data, x.(*Signature).data) == 1
}

// PointG1 returns the point on G1 for the signature.
Expand Down
1 change: 1 addition & 0 deletions crypto/ed25519/ed25519.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package ed25519
186 changes: 186 additions & 0 deletions crypto/ed25519/ed25519_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package ed25519_test

import (
"encoding/hex"
"testing"

bls12381 "github.com/kilic/bls12-381"
"github.com/pactus-project/pactus/crypto"
"github.com/pactus-project/pactus/crypto/bls"
"github.com/pactus-project/pactus/util/testsuite"
"github.com/stretchr/testify/assert"
)

func TestSigning(t *testing.T) {
msg := []byte("zarb")
prv, _ := bls.PrivateKeyFromString(
"SECRET1PDRWTLP5PX0FAHDX39GXZJP7FKZFALML0D5U9TT9KVQHDUC99CMGQQJVK67")
pub, _ := bls.PublicKeyFromString(
"public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjr" +
"vqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5jhx47a")
sig, _ := bls.SignatureFromString(
"ad0f88cec815e9b8af3f0136297cb242ed8b6369af723fbdac077fa927f5780db7df47c77fb53f3a22324673f000c792")
addr, _ := crypto.AddressFromString("pc1p5x2a0lkt5nrrdqe0rkcv6r4pfkmdhrr3xk73tq")

sig1 := prv.Sign(msg)
assert.Equal(t, sig.Bytes(), sig1.Bytes())
assert.NoError(t, pub.Verify(msg, sig))
assert.Equal(t, pub, prv.PublicKey())
assert.Equal(t, addr, pub.ValidatorAddress())
}

func TestSignatureAggregate(t *testing.T) {
msg := []byte("zarb")
prv1, _ := bls.PrivateKeyFromString(
"SECRET1PDRWTLP5PX0FAHDX39GXZJP7FKZFALML0D5U9TT9KVQHDUC99CMGQQJVK67")
prv2, _ := bls.PrivateKeyFromString(
"SECRET1PDUV97560CWDGW2DR453YPUT84REN04G0DZFAPJQL5DV0CKDAN75QCJEV6F")
agg, _ := bls.SignatureFromString(
"a390ffec7061827b7e89193a26841dd9e3537b5db0af55661b624e8b93b855e9f65278850002ea72fb3098e674220eca")
sig1 := prv1.Sign(msg).(*bls.Signature)
sig2 := prv2.Sign(msg).(*bls.Signature)

assert.True(t, bls.SignatureAggregate(sig1, sig2).EqualsTo(agg))
assert.False(t, prv1.EqualsTo(prv2))
}

func TestAggregateFailed(t *testing.T) {
ts := testsuite.NewTestSuite(t)

pub1, prv1 := ts.RandBLSKeyPair()
pub2, prv2 := ts.RandBLSKeyPair()
pub3, prv3 := ts.RandBLSKeyPair()
pub4, prv4 := ts.RandBLSKeyPair()
msg1 := []byte("zarb")
msg2 := []byte("zarb0")

sig1 := prv1.Sign(msg1).(*bls.Signature)
sig11 := prv1.Sign(msg2).(*bls.Signature)
sig2 := prv2.Sign(msg1).(*bls.Signature)
sig3 := prv3.Sign(msg1).(*bls.Signature)
sig4 := prv4.Sign(msg1).(*bls.Signature)

agg1 := bls.SignatureAggregate(sig1, sig2, sig3)
agg2 := bls.SignatureAggregate(sig1, sig2, sig4)
agg3 := bls.SignatureAggregate(sig11, sig2, sig3)
agg4 := bls.SignatureAggregate(sig1, sig2)
agg5 := bls.SignatureAggregate(sig3, sig2, sig1)

pubs1 := []*bls.PublicKey{pub1, pub2, pub3}
pubs2 := []*bls.PublicKey{pub1, pub2, pub4}
pubs3 := []*bls.PublicKey{pub1, pub2}
pubs4 := []*bls.PublicKey{pub3, pub2, pub1}

pubAgg1 := bls.PublicKeyAggregate(pubs1...)
pubAgg2 := bls.PublicKeyAggregate(pubs2...)
pubAgg3 := bls.PublicKeyAggregate(pubs3...)
pubAgg4 := bls.PublicKeyAggregate(pubs4...)

assert.NoError(t, pub1.Verify(msg1, sig1))
assert.NoError(t, pub2.Verify(msg1, sig2))
assert.NoError(t, pub3.Verify(msg1, sig3))
assert.Error(t, pub2.Verify(msg1, sig1))
assert.Error(t, pub3.Verify(msg1, sig1))
assert.Error(t, pub1.Verify(msg1, agg1))
assert.Error(t, pub2.Verify(msg1, agg1))
assert.Error(t, pub3.Verify(msg1, agg1))

assert.NoError(t, pubAgg1.Verify(msg1, agg1))
assert.Error(t, pubAgg1.Verify(msg2, agg1))
assert.Error(t, pubAgg1.Verify(msg1, agg2))
assert.Error(t, pubAgg2.Verify(msg1, agg1))
assert.NoError(t, pubAgg2.Verify(msg1, agg2))
assert.Error(t, pubAgg2.Verify(msg2, agg2))
assert.Error(t, pubAgg1.Verify(msg1, agg3))
assert.Error(t, pubAgg1.Verify(msg2, agg3))
assert.Error(t, pubAgg1.Verify(msg1, agg4))
assert.Error(t, pubAgg3.Verify(msg1, agg1))
assert.NoError(t, pubAgg1.Verify(msg1, agg5))
assert.NoError(t, pubAgg4.Verify(msg1, agg1))
}

func TestAggregateOnlyOneSignature(t *testing.T) {
ts := testsuite.NewTestSuite(t)

_, prv1 := ts.RandBLSKeyPair()
msg1 := []byte("zarb")
sig1 := prv1.Sign(msg1).(*bls.Signature)
agg1 := bls.SignatureAggregate(sig1)

assert.True(t, agg1.EqualsTo(sig1))
}

func TestAggregateOnlyOnePublicKey(t *testing.T) {
ts := testsuite.NewTestSuite(t)

pub1, _ := ts.RandBLSKeyPair()
agg1 := bls.PublicKeyAggregate(pub1)

assert.True(t, agg1.EqualsTo(pub1))
}

// TODO: should we check for duplication here?
func TestDuplicatedAggregate(t *testing.T) {
ts := testsuite.NewTestSuite(t)

pub1, prv1 := ts.RandBLSKeyPair()
pub2, prv2 := ts.RandBLSKeyPair()

msg1 := []byte("zarb")

sig1 := prv1.Sign(msg1).(*bls.Signature)
sig2 := prv2.Sign(msg1).(*bls.Signature)

agg1 := bls.SignatureAggregate(sig1, sig2, sig1)
agg2 := bls.SignatureAggregate(sig1, sig2)
assert.False(t, agg1.EqualsTo(agg2))

pubs1 := []*bls.PublicKey{pub1, pub2}
pubs2 := []*bls.PublicKey{pub1, pub2, pub1}
pubAgg1 := bls.PublicKeyAggregate(pubs1...)
pubAgg2 := bls.PublicKeyAggregate(pubs2...)
assert.False(t, pubAgg1.EqualsTo(pubAgg2))
}

// TestHashToCurve ensures that the hash-to-curve function in kilic/bls12-381
// works as intended and is compatible with the spec.
// test vectors can be found here:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#appendix-J.9.1
func TestHashToCurve(t *testing.T) {
domain := []byte("QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_")
tests := []struct {
msg string
expected string
}{
{
"",
"052926add2207b76ca4fa57a8734416c8dc95e24501772c814278700eed6d1e4e8cf62d9c09db0fac349612b759e79a1" +
"08ba738453bfed09cb546dbb0783dbb3a5f1f566ed67bb6be0e8c67e2e81a4cc68ee29813bb7994998f3eae0c9c6a265",
},
{
"abc",
"03567bc5ef9c690c2ab2ecdf6a96ef1c139cc0b2f284dca0a9a7943388a49a3aee664ba5379a7655d3c68900be2f6903" +
"0b9c15f3fe6e5cf4211f346271d7b01c8f3b28be689c8429c85b67af215533311f0b8dfaaa154fa6b88176c229f2885d",
},
{
"abcdef0123456789",
"11e0b079dea29a68f0383ee94fed1b940995272407e3bb916bbf268c263ddd57a6a27200a784cbc248e84f357ce82d98" +
"03a87ae2caf14e8ee52e51fa2ed8eefe80f02457004ba4d486d6aa1f517c0889501dc7413753f9599b099ebcbbd2d709",
},
{
"q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" +
"qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"15f68eaa693b95ccb85215dc65fa81038d69629f70aeee0d0f677cf22285e7bf58d7cb86eefe8f2e9bc3f8cb84fac488" +
"1807a1d50c29f430b8cafc4f8638dfeeadf51211e1602a5f184443076715f91bb90a48ba1e370edce6ae1062f5e6dd38",
},
}

g1 := bls12381.NewG1()
for no, test := range tests {
mappedPoint, _ := g1.HashToCurve([]byte(test.msg), domain)
d, _ := hex.DecodeString(test.expected)
expectedPoint, _ := g1.FromBytes(d)
assert.Equal(t, expectedPoint, mappedPoint,
"test %v: not match", no)
}
}
1 change: 1 addition & 0 deletions crypto/ed25519/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package ed25519
Loading

0 comments on commit 4eb5335

Please sign in to comment.