diff --git a/crypto/address.go b/crypto/address.go index 1a79762e1..06c450d28 100644 --- a/crypto/address.go +++ b/crypto/address.go @@ -20,7 +20,8 @@ const ( ) const ( - SignatureTypeBLS byte = 1 + SignatureTypeBLS byte = 1 + SignatureTypeEd25519 byte = 3 ) const ( diff --git a/crypto/bls/hdkeychain/errors.go b/crypto/bls/hdkeychain/errors.go index 110dfad53..a644ee6eb 100644 --- a/crypto/bls/hdkeychain/errors.go +++ b/crypto/bls/hdkeychain/errors.go @@ -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") ) diff --git a/crypto/bls/hdkeychain/extendedkey.go b/crypto/bls/hdkeychain/extendedkey.go index 34fd8c715..087c272a0 100644 --- a/crypto/bls/hdkeychain/extendedkey.go +++ b/crypto/bls/hdkeychain/extendedkey.go @@ -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 ( @@ -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 diff --git a/crypto/bls/hdkeychain/extendedkey_test.go b/crypto/bls/hdkeychain/extendedkey_test.go index 1932ed978..478fc1690 100644 --- a/crypto/bls/hdkeychain/extendedkey_test.go +++ b/crypto/bls/hdkeychain/extendedkey_test.go @@ -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) } } } @@ -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, }, } diff --git a/crypto/bls/private_key.go b/crypto/bls/private_key.go index 456940f0a..f0f8a5680 100644 --- a/crypto/bls/private_key.go +++ b/crypto/bls/private_key.go @@ -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) } diff --git a/crypto/bls/public_key.go b/crypto/bls/public_key.go index 722ac3a87..63b4c0157 100644 --- a/crypto/bls/public_key.go +++ b/crypto/bls/public_key.go @@ -2,6 +2,7 @@ package bls import ( "bytes" + "crypto/subtle" "fmt" "io" @@ -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. diff --git a/crypto/bls/signature.go b/crypto/bls/signature.go index 1ceccfe66..2cf6c1c8b 100644 --- a/crypto/bls/signature.go +++ b/crypto/bls/signature.go @@ -2,6 +2,7 @@ package bls import ( "bytes" + "crypto/subtle" "encoding/hex" "fmt" "io" @@ -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. diff --git a/crypto/ed25519/ed25519.go b/crypto/ed25519/ed25519.go new file mode 100644 index 000000000..21984ca9c --- /dev/null +++ b/crypto/ed25519/ed25519.go @@ -0,0 +1 @@ +package ed25519 diff --git a/crypto/ed25519/ed25519_test.go b/crypto/ed25519/ed25519_test.go new file mode 100644 index 000000000..6a065c139 --- /dev/null +++ b/crypto/ed25519/ed25519_test.go @@ -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) + } +} diff --git a/crypto/ed25519/errors.go b/crypto/ed25519/errors.go new file mode 100644 index 000000000..21984ca9c --- /dev/null +++ b/crypto/ed25519/errors.go @@ -0,0 +1 @@ +package ed25519 diff --git a/crypto/ed25519/hdkeychain/errors.go b/crypto/ed25519/hdkeychain/errors.go new file mode 100644 index 000000000..acd944e0b --- /dev/null +++ b/crypto/ed25519/hdkeychain/errors.go @@ -0,0 +1,24 @@ +package hdkeychain + +import ( + "errors" + "fmt" +) + +var ( + // ErrInvalidSeedLen describes an error in which the provided seed or + // seed length is not in the allowed range. + ErrInvalidSeedLen = fmt.Errorf("seed length must be between %d and %d "+ + "bits", MinSeedBytes*8, MaxSeedBytes*8) + + // 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") + + // ErrNonHardenedPath is returned when a non-hardened derivation path is used, + // which is not supported by ed25519. + ErrNonHardenedPath = errors.New("non-hardened derivation not supported") +) diff --git a/crypto/ed25519/hdkeychain/extendedkey.go b/crypto/ed25519/hdkeychain/extendedkey.go new file mode 100644 index 000000000..02846dbe3 --- /dev/null +++ b/crypto/ed25519/hdkeychain/extendedkey.go @@ -0,0 +1,279 @@ +package hdkeychain + +// References: +// SLIP-0010: Universal private key derivation from master private key +// https://github.com/satoshilabs/slips/blob/master/slip-0010.md + +import ( + "bytes" + "crypto/ed25519" + "crypto/hmac" + "crypto/rand" + "crypto/sha512" + "encoding/binary" + "strings" + + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/util/bech32m" + "github.com/pactus-project/pactus/util/encoding" +) + +const ( + // HardenedKeyStart is the index at which a hardened key starts. + HardenedKeyStart = uint32(0x80000000) // 2^31 + + // MinSeedBytes is the minimum number of bytes allowed for a seed to + // a master node. + MinSeedBytes = 16 // 128 bits + + // MaxSeedBytes is the maximum number of bytes allowed for a seed to + // a master node. + MaxSeedBytes = 64 // 512 bits +) + +// ExtendedKey houses all the information needed to support a hierarchical +// deterministic extended key. +type ExtendedKey struct { + key []byte // This will be the bytes of extended public or private key + chainCode []byte + path []uint32 +} + +// newExtendedKey returns a new instance of an extended key with the given +// fields. No error checking is performed here as it's only intended to be a +// convenience method used to create a populated struct. +func newExtendedKey(key, chainCode []byte, path []uint32) *ExtendedKey { + return &ExtendedKey{ + key: key, + chainCode: chainCode, + path: path, + } +} + +// DerivePath returns a derived child extended key from this master key at the +// given path. +func (k *ExtendedKey) DerivePath(path []uint32) (*ExtendedKey, error) { + ext := k + var err error + for _, index := range path { + ext, err = ext.Derive(index) + if err != nil { + return nil, err + } + } + + return ext, nil +} + +// Derive returns a derived child extended key at the given index. +// +// For ed25519 and curve25519 the private keys are no longer multipliers for the group generator; +// instead the hash of the private key is the multiplier. +// For this reason, our scheme for ed25519 and curve25519 does not support public key derivation and +// uses the produced hashes directly as private keys. +func (k *ExtendedKey) Derive(index uint32) (*ExtendedKey, error) { + isChildHardened := index >= HardenedKeyStart + + if !isChildHardened { + return nil, ErrNonHardenedPath + } + + // Calculate derive Data: + // Data = 0x00 || ser_256(k_par) || ser_32(i) + indexData := make([]byte, 4) + binary.BigEndian.PutUint32(indexData, index) + + data := make([]byte, 0, 37) + data = append(data, 0x00) + data = append(data, k.key...) + data = append(data, indexData...) + + // Take the HMAC-SHA512 of the current key's chain code and the derived + // data: + // I = HMAC-SHA512(Key = chainCode, Data = data) + hmac512 := hmac.New(sha512.New, k.chainCode) + _, _ = hmac512.Write(data) + ilr := hmac512.Sum(nil) + + // Split I into two 32-byte sequences, IL and IR. + // The returned chain code ci is IR. + // The returned child key ki is IL. + childChainCode := ilr[32:] + childKey := ilr[:32] + + newPath := make([]uint32, 0, len(k.path)+1) + newPath = append(newPath, k.path...) + newPath = append(newPath, index) + + return newExtendedKey(childKey, childChainCode, newPath), nil +} + +// Path returns the path of derived key. +// +// Path values are always between 2^31 and 2^32-1 as they are hardened keys. +func (k *ExtendedKey) Path() []uint32 { + return k.path +} + +// RawPrivateKey returns the raw bytes of the private key. +func (k *ExtendedKey) RawPrivateKey() []byte { + return k.key +} + +// RawPublicKey returns the raw bytes of the public key. +func (k *ExtendedKey) RawPublicKey() []byte { + pub := ed25519.NewKeyFromSeed(k.key).Public() + + return pub.(ed25519.PublicKey)[:] +} + +// String returns the extended key as a bech32-encoded string. +func (k *ExtendedKey) String() string { + // + // The serialized format is structured as follows: + // +-------+---------+------------+----------+------------+----------+ + // | Depth | Path | Chain code | Reserved | Key length | Key data | + // +-------+---------+------------+----------+------------+----------+ + // | 1 | depth*4 | 32 | 1 | 1 | 32 | + // +-------+---------+------------+----------+------------+----------+ + // + // Description: + // - Depth: 1 byte representing the depth of derivation path. + // - Path: serialized BIP-32 path; each entry is encoded as 32-bit unsigned integer, least significant byte first + // - Chain code: 32 bytes chain code + // - Reserved: 1 byte reserved and should set to 0. + // - Key length: 1 byte representing the length of the key data that is 32. + // - Key data: The key data that is 32 bytes. + // + + w := bytes.NewBuffer(make([]byte, 0)) + err := encoding.WriteElement(w, byte(len(k.path))) + if err != nil { + return err.Error() + } + + for _, p := range k.path { + err := encoding.WriteElement(w, p) + if err != nil { + return err.Error() + } + } + err = encoding.WriteVarBytes(w, k.chainCode) + if err != nil { + return err.Error() + } + + err = encoding.WriteElement(w, uint8(0)) + if err != nil { + return err.Error() + } + + err = encoding.WriteVarBytes(w, k.key) + if err != nil { + return err.Error() + } + + str, err := bech32m.EncodeFromBase256WithType(crypto.XPrivateKeyHRP, crypto.SignatureTypeEd25519, w.Bytes()) + if err != nil { + return err.Error() + } + + str = strings.ToUpper(str) + + return str +} + +// NewKeyFromString returns a new extended key instance from a bech32-encoded string. +func NewKeyFromString(str string) (*ExtendedKey, error) { + hrp, typ, data, err := bech32m.DecodeToBase256WithTypeNoLimit(strings.ToLower(str)) + if err != nil { + return nil, err + } + + if typ != crypto.SignatureTypeEd25519 { + return nil, ErrInvalidKeyData + } + + if hrp != crypto.XPrivateKeyHRP { + return nil, ErrInvalidHRP + } + + r := bytes.NewReader(data) + depth := uint8(0) + err = encoding.ReadElement(r, &depth) + if err != nil { + return nil, err + } + + path := make([]uint32, depth) + for i := byte(0); i < depth; i++ { + err := encoding.ReadElement(r, &path[i]) + if err != nil { + return nil, err + } + } + + chainCode, err := encoding.ReadVarBytes(r) + if err != nil { + return nil, err + } + + var res uint8 + err = encoding.ReadElement(r, &res) + if err != nil { + return nil, err + } + + key, err := encoding.ReadVarBytes(r) + if err != nil { + return nil, err + } + + return newExtendedKey(key, chainCode, path), nil +} + +// NewMaster creates a new master node for use in creating a hierarchical +// deterministic key chain. The seed must be between 128 and 512 bits and +// should be generated by a cryptographically secure random generation source. +func NewMaster(seed []byte) (*ExtendedKey, error) { + // Per [BIP32], the seed must be in range [MinSeedBytes, MaxSeedBytes]. + if len(seed) < MinSeedBytes || len(seed) > MaxSeedBytes { + return nil, ErrInvalidSeedLen + } + + // First take the HMAC-SHA512 of the master key and the seed data: + // I = HMAC-SHA512(Key = Curve, Data = Seed) + curve := []byte("ed25519 seed") + hmac512 := hmac.New(sha512.New, curve) + _, _ = hmac512.Write(seed) + lr := hmac512.Sum(nil) + + // Split "I" into two 32-byte sequences Il and Ir where: + // Il = master key + // Ir = master chain code + masterChainCode := lr[32:] + masterKey := lr[:32] + + return newExtendedKey(masterKey, masterChainCode, []uint32{}), nil +} + +// GenerateSeed returns a cryptographically secure random seed that can be used +// as the input for the NewMaster function to generate a new master node. +// +// The length is in bytes and it must be between 16 and 64 (128 to 512 bits). +// The recommended length is 32 (256 bits) as defined by the RecommendedSeedLen +// constant. +func GenerateSeed(length uint8) ([]byte, error) { + // Per [BIP32], the seed must be in range [MinSeedBytes, MaxSeedBytes]. + if length < MinSeedBytes || length > MaxSeedBytes { + return nil, ErrInvalidSeedLen + } + + buf := make([]byte, length) + _, err := rand.Read(buf) + if err != nil { + return nil, err + } + + return buf, nil +} diff --git a/crypto/ed25519/hdkeychain/extendedkey_test.go b/crypto/ed25519/hdkeychain/extendedkey_test.go new file mode 100644 index 000000000..bb7e87073 --- /dev/null +++ b/crypto/ed25519/hdkeychain/extendedkey_test.go @@ -0,0 +1,314 @@ +package hdkeychain + +import ( + "encoding/hex" + "io" + "testing" + + "github.com/pactus-project/pactus/util/bech32m" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestNonHardenedDerivation tests deriving a new key in non-hardened mode. +// It should return an error. +func TestNonHardenedDerivation(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + testSeed := ts.RandBytes(32) + path := []uint32{ + ts.RandUint32(HardenedKeyStart), + } + + masterKey, _ := NewMaster(testSeed) + _, err := masterKey.DerivePath(path) + assert.ErrorIs(t, err, ErrNonHardenedPath) +} + +// TestHardenedDerivation tests derive key in hardened mode. +func TestHardenedDerivation(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + testSeed := ts.RandBytes(32) + path := []uint32{ + ts.RandUint32(HardenedKeyStart) + HardenedKeyStart, + } + + masterKey, err := NewMaster(testSeed) + require.NoError(t, err) + + extKey, err := masterKey.DerivePath(path) + require.NoError(t, err) + + assert.Equal(t, path, extKey.Path()) +} + +// TestDerivation verifies the derivation of new keys in hardened mode. +// The test cases are based on the SLIP-0010 standard. +func TestDerivation(t *testing.T) { + testSeed, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f") + h := HardenedKeyStart + tests := []struct { + name string + path []uint32 + wantPrv string + wantPub string + }{ + { + name: "derivation path: m", + path: []uint32{}, + wantPrv: "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7", + wantPub: "a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed", + }, + { + name: "derivation path: m/0H", + path: []uint32{h}, + wantPrv: "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3", + wantPub: "8c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c", + }, + { + name: "derivation path: m/0H/1H", + path: []uint32{h, 1 + h}, + wantPrv: "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2", + wantPub: "1932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187", + }, + { + name: "derivation path: m/0H/1H/2H", + path: []uint32{h, 1 + h, 2 + h}, + wantPrv: "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9", + wantPub: "ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1", + }, + { + name: "derivation path: m/0H/1H/2H/2H", + path: []uint32{h, 1 + h, 2 + h, 2 + h}, + wantPrv: "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662", + wantPub: "8abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c", + }, + { + name: "derivation path: m/0H/1H/2H/2H/1000000000H", + path: []uint32{h, 1 + h, 2 + h, 2 + h, 1000000000 + h}, + wantPrv: "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793", + wantPub: "3c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a", + }, + } + + masterKey, _ := NewMaster(testSeed) + for i, test := range tests { + extKey, err := masterKey.DerivePath(test.path) + require.NoError(t, err) + + privKey := extKey.RawPrivateKey() + require.Equal(t, test.wantPrv, hex.EncodeToString(privKey), + "mismatched serialized private key for test #%v", i+1) + + pubKey := extKey.RawPublicKey() + require.Equal(t, test.wantPub, hex.EncodeToString(pubKey), + "mismatched serialized public key for test #%v", i+1) + + require.Equal(t, test.path, extKey.Path()) + } +} + +// TestGenerateSeed ensures the GenerateSeed function works as intended. +func TestGenerateSeed(t *testing.T) { + tests := []struct { + name string + length uint8 + err error + }{ + // Test various valid lengths. + {name: "16 bytes", length: 16}, + {name: "17 bytes", length: 17}, + {name: "20 bytes", length: 20}, + {name: "32 bytes", length: 32}, + {name: "64 bytes", length: 64}, + + // Test invalid lengths. + {name: "15 bytes", length: 15, err: ErrInvalidSeedLen}, + {name: "65 bytes", length: 65, err: ErrInvalidSeedLen}, + } + + for i, test := range tests { + seed, err := GenerateSeed(test.length) + assert.ErrorIs(t, err, test.err) + + if test.err == nil { + assert.Len(t, seed, int(test.length), + "GenerateSeed #%d (%s): length mismatch -- got %d, want %d", + i+1, 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 + key string + err error + }{ + // Test various valid seeds. + { + name: "16 bytes", + seed: "000102030405060708090a0b0c0d0e0f", + key: "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7", + }, + { + name: "32 bytes", + seed: "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678", + key: "4b36bc63a15797f4d506074f36f2f3904bc0f10179b5ab91183c167e9c2dcf0e", + }, + { + name: "64 bytes", + seed: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784" + + "817e7b7875726f6c696663605d5a5754514e4b484542", + key: "171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012", + }, + + // Test invalid seeds. + { + name: "empty seed", + seed: "", + err: ErrInvalidSeedLen, + }, + { + name: "15 bytes", + seed: "000000000000000000000000000000", + err: ErrInvalidSeedLen, + }, + { + name: "65 bytes", + seed: "000000000000000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000", + err: ErrInvalidSeedLen, + }, + } + + for i, test := range tests { + seed, _ := hex.DecodeString(test.seed) + extKey, err := NewMaster(seed) + assert.ErrorIs(t, err, test.err) + + if test.err == nil { + privKey := extKey.RawPrivateKey() + assert.Equal(t, test.key, hex.EncodeToString(privKey), + "NewMaster #%d (%s): key mismatch -- got %x, want %s", + i+1, test.name, privKey, test.key) + } + } +} + +// TestKeyToString ensures the String function works as intended. +// +//nolint:lll // long extended keys +func TestKeyToString(t *testing.T) { + testSeed, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f") + h := HardenedKeyStart + tests := []struct { + name string + path []uint32 + wantXPriv string + }{ + { + name: "derivation path: m", + path: []uint32{}, + wantXPriv: "XSECRET1RQQSFQPR2J0098Q989D0Y2QG8FPT86H4Q9WLK2GHE08S9CRVD3J5LL7CQYQ45HEL3NM38H0ESCENMVSK47J4XNLGKNPE03LPST8QGAWHZAVV7WQ3TTQ3", + }, + { + name: "derivation path: m/0H", + path: []uint32{h}, + wantXPriv: "XSECRET1RQYQQQQYQYZ94N2S38Q9KYN5P2PAZ0LKA5K075MGTW7D80ZGC5T7NTY8PD6WXJQPQDRS0U3KLKELRDRR4X7DVA3V3MTGEMU7DUFHX8WF63ECY78DDU73SSP8VGW", + }, + { + name: "derivation path: m/0H/1H", + path: []uint32{h, 1 + h}, + wantXPriv: "XSECRET1RQGQQQQYQQYQQPQPQ5VSYYHMH6X6UY5Z6DVDJWWPTXUMGAEJQUD2HCV25Z6QPYS649U2QQG936ZADGP9LXHD8SKNYEGDV2JEXZUS36FMHD9HML7HJPRM5DT5Y7GG0AYKR", + }, + { + name: "derivation path: m/0H/1H/2H", + path: []uint32{h, 1 + h, 2 + h}, + wantXPriv: "XSECRET1RQVQQQQYQQYQQPQQZQQQGQGPWDXFFUQ944VJS7JWRLVWP9UJJME876TQAHZPCWZ22P7XYE8XDDSQZPY49KG7QHZ5EUD7S0HELHXTXJ9L46PHQ9HDAJZW8UXZRW9RRA87FNLUP3Y", + }, + { + name: "derivation path: m/0H/1H/2H/2H", + path: []uint32{h, 1 + h, 2 + h, 2 + h}, + wantXPriv: "XSECRET1RQSQQQQYQQYQQPQQZQQQGQQSQQZQZPRMDSLUN6AGWPM7VMGQH6E32RVC6YEHY5M6EJWC47HQLQLM5M4WVQQSRP5WU0E0UQNP3YXDTYK384CQT2RM06ENZ9AHFEYFJ20T9Z8G7VCSW77AAC", + }, + { + name: "derivation path: m/0H/1H/2H/2H/1000000000H", + path: []uint32{h, 1 + h, 2 + h, 2 + h, 1000000000 + h}, + wantXPriv: "XSECRET1RQ5QQQQYQQYQQPQQZQQQGQQSQQZQQPJ56HVSXS7YEYWSV4SKDTG53W2J8TL57P7C5E44DKKKE3GL6WQENU7H6YVQQYZ8EF5U54R5066CMCTELF86UGL3C22QATST7V5EYKRMZFQLR06REXGPVGYE", + }, + } + + masterKey, _ := NewMaster(testSeed) + for i, test := range tests { + extKey, _ := masterKey.DerivePath(test.path) + require.Equal(t, test.wantXPriv, extKey.String(), "test %d failed", i) + + recoveredExtKey, err := NewKeyFromString(test.wantXPriv) + require.NoError(t, err) + + require.Equal(t, extKey, recoveredExtKey) + require.Equal(t, test.path, recoveredExtKey.path) + } +} + +// TestInvalidString checks errors corresponding to the invalid strings +// +//nolint:lll // long extended private keys +func TestInvalidString(t *testing.T) { + tests := []struct { + desc string + str string + expectedError error + }{ + { + desc: "invalid checksum", + str: "XSECRET1RQGQQQQYQQYQQPQPQ5VSYYHMH6X6UY5Z6DVDJWWPTXUMGAEJQUD2HCV25Z6QPYS649U2QQG936ZADGP9LXHD8SKNYEGDV2JEXZUS36FMHD9HML7HJPRM5DT5Y7GG0AYRK", + expectedError: bech32m.InvalidChecksumError{Expected: "g0aykr", Actual: "g0ayrk"}, + }, + { + desc: "no depth", + str: "XSECRET1RFK28CY", + expectedError: io.EOF, + }, + { + desc: "wrong path", + str: "XSECRET1RQGQQQQYQ6EJ6DE", + expectedError: io.EOF, + }, + { + desc: "no chain code", + str: "XSECRET1RQGQQQQYQQYQQPQQ98TS98", + expectedError: io.EOF, + }, + { + desc: "no reserved", + str: "XSECRET1RQGQQQQYQQYQQPQPQ5VSYYHMH6X6UY5Z6DVDJWWPTXUMGAEJQUD2HCV25Z6QPYS649U2Q8GUZZJ", + expectedError: io.EOF, + }, + { + desc: "no key", + str: "XSECRET1RQGQQQQYQQYQQPQPQ5VSYYHMH6X6UY5Z6DVDJWWPTXUMGAEJQUD2HCV25Z6QPYS649U2QQ85HSJA", + expectedError: io.EOF, + }, + { + desc: "invalid type", + str: "XSECRET1YQGQQQQYQQYQQPQPQ5VSYYHMH6X6UY5Z6DVDJWWPTXUMGAEJQUD2HCV25Z6QPYS649U2QQG936ZADGP9LXHD8SKNYEGDV2JEXZUS36FMHD9HML7HJPRM5DT5Y7GTKSQQT", + expectedError: ErrInvalidKeyData, + }, + { + desc: "invalid hrp", + str: "SECRET1RQGQQQQYQQYQQPQPQ5VSYYHMH6X6UY5Z6DVDJWWPTXUMGAEJQUD2HCV25Z6QPYS649U2QQG936ZADGP9LXHD8SKNYEGDV2JEXZUS36FMHD9HML7HJPRM5DT5Y7GYQ7VAT", + expectedError: ErrInvalidHRP, + }, + } + + for i, test := range tests { + _, err := NewKeyFromString(test.str) + assert.ErrorIs(t, err, test.expectedError, "test %d error is not matched", i) + } +} diff --git a/crypto/ed25519/private_key.go b/crypto/ed25519/private_key.go new file mode 100644 index 000000000..0a0820dde --- /dev/null +++ b/crypto/ed25519/private_key.go @@ -0,0 +1,108 @@ +package ed25519 + +import ( + "crypto/ed25519" + "strings" + + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/util/bech32m" + "github.com/pactus-project/pactus/util/errors" +) + +var _ crypto.PrivateKey = &PrivateKey{} + +const PrivateKeySize = 32 + +type PrivateKey struct { + inner ed25519.PrivateKey +} + +// PrivateKeyFromString decodes the input string and returns the PrivateKey +// if the string is a valid bech32m encoding of a BLS public key. +func PrivateKeyFromString(text string) (*PrivateKey, error) { + // Decode the bech32m encoded private key. + hrp, typ, data, err := bech32m.DecodeToBase256WithTypeNoLimit(text) + if err != nil { + return nil, err + } + + // Check if hrp is valid + if hrp != crypto.PrivateKeyHRP { + return nil, crypto.InvalidHRPError(hrp) + } + + if typ != crypto.SignatureTypeEd25519 { + return nil, errors.Errorf(errors.ErrInvalidPrivateKey, + "invalid private key type: %v", typ) + } + + return PrivateKeyFromBytes(data) +} + +func KeyGen(seed []byte) (*PrivateKey, error) { + prv := ed25519.NewKeyFromSeed(seed) + + return PrivateKeyFromBytes(prv) +} + +// PrivateKeyFromBytes constructs a ED25519 private key from the raw bytes. +func PrivateKeyFromBytes(data []byte) (*PrivateKey, error) { + if len(data) != PrivateKeySize { + return nil, errors.Errorf(errors.ErrInvalidPrivateKey, + "private key should be %d bytes, but it is %v bytes", PrivateKeySize, len(data)) + } + inner := ed25519.NewKeyFromSeed(data) + + return &PrivateKey{inner}, nil +} + +// String returns a human-readable string for the ED25519 private key. +func (prv *PrivateKey) String() string { + str, _ := bech32m.EncodeFromBase256WithType( + crypto.PrivateKeyHRP, + crypto.SignatureTypeEd25519, + prv.Bytes()) + + return strings.ToUpper(str) +} + +// Bytes return the raw bytes of the private key. +func (prv *PrivateKey) Bytes() []byte { + return prv.inner[:PrivateKeySize] +} + +// Sign calculates the signature from the private key and given message. +// It's defined in section 2.6 of the spec: CoreSign. +func (prv *PrivateKey) Sign(msg []byte) crypto.Signature { + return prv.SignNative(msg) +} + +func (prv *PrivateKey) SignNative(msg []byte) *Signature { + sig := ed25519.Sign(prv.inner, msg) + + return &Signature{ + data: sig, + } +} + +func (prv *PrivateKey) PublicKeyNative() *PublicKey { + pub := prv.inner.Public() + + // TODO: fix me, should get from scalar multiplication. + return &PublicKey{ + inner: pub.(ed25519.PublicKey), + } +} + +func (prv *PrivateKey) PublicKey() crypto.PublicKey { + return prv.PublicKeyNative() +} + +func (prv *PrivateKey) EqualsTo(x crypto.PrivateKey) bool { + xEd25519, ok := x.(*PrivateKey) + if !ok { + return false + } + + return prv.inner.Equal(xEd25519.inner) +} diff --git a/crypto/ed25519/private_key_test.go b/crypto/ed25519/private_key_test.go new file mode 100644 index 000000000..447650688 --- /dev/null +++ b/crypto/ed25519/private_key_test.go @@ -0,0 +1,105 @@ +package ed25519_test + +import ( + "encoding/hex" + "fmt" + "strings" + "testing" + + "github.com/pactus-project/pactus/crypto/ed25519" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" +) + +func TestPrivateKeyEqualsTo(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + _, prv1 := ts.RandED25519KeyPair() + _, prv2 := ts.RandED25519KeyPair() + + fmt.Println(prv1.String()) + + assert.True(t, prv1.EqualsTo(prv1)) + assert.False(t, prv1.EqualsTo(prv2)) + assert.Equal(t, prv1, prv1) + assert.NotEqual(t, prv1, prv2) +} + +func TestPrivateKeyFromString(t *testing.T) { + tests := []struct { + errMsg string + encoded string + valid bool + result []byte + }{ + { + "invalid separator index -1", + "XXXXXXR2SYCC5TDQKMJ73J64J8GJTMTKREEQNQAS0M5SLZ9LVJV7Y940NVZQD9JUS" + + "GV2N44C9H5PVGRXARNGZ7QF3PSKH7805E5SZXPE7ZHHAGX0NFQR", + false, nil, + }, + { + "invalid checksum (expected s9c56g got czlgh0)", + "SECRET1RAC7048K666DCCYG7FJW68ZE2G6P32UAPLRLWDV3RTAR4PWZUX2CDSFAL55VM" + + "YS06CY35LA72AWZN5DY5NZA078S4S4K654UFJ0YCCZLGH0", + false, nil, + }, + { + "invalid bech32 string length 0", + "", + false, nil, + }, + { + "invalid character not part of charset: 105", + "SECRET1IOIOOI", + false, nil, + }, + { + "invalid bech32 string length 0", + "SECRET1HPZZU9", + false, nil, + }, + { + "", + "SECRET1RJ6STNTA7Y3P2QLQF8A6QCX05F2H5TFNE5RSH066KZME4WVFXKE7QW097LG", + true, + []byte{ + 0x96, 0xa0, 0xb9, 0xaf, 0xbe, 0x24, 0x42, 0xa0, 0x7c, 0x9, 0x3f, 0x74, 0xc, 0x19, 0xf4, + 0x4a, 0xaf, 0x45, 0xa6, 0x79, 0xa0, 0xe1, 0x77, 0xeb, 0x56, 0x16, 0xf3, 0x57, 0x31, 0x26, + 0xb6, 0x7c, + }, + }, + } + + for no, test := range tests { + prv, err := ed25519.PrivateKeyFromString(test.encoded) + if test.valid { + assert.NoError(t, err, "test %v: unexpected error", no) + assert.Equal(t, test.result, prv.Bytes(), "test %v: invalid bytes", no) + assert.Equal(t, strings.ToUpper(test.encoded), prv.String(), "test %v: invalid encoded", no) + } else { + assert.Contains(t, err.Error(), test.errMsg, "test %v: error not matched", no) + } + } +} + +// TestKeyGen ensures the KeyGen function works as intended. +func TestKeyGen(t *testing.T) { + tests := []struct { + seed []byte + sk string + }{} + + for i, test := range tests { + prv, err := ed25519.KeyGen(test.seed) + if test.sk == "Err" { + assert.Error(t, err, + "test '%v' failed. no error", i) + } else { + assert.NoError(t, err, + "test'%v' failed. has error", i) + assert.Equal(t, test.sk, hex.EncodeToString(prv.Bytes()), + "test '%v' failed. not equal", i) + } + } +} diff --git a/crypto/ed25519/public_key.go b/crypto/ed25519/public_key.go new file mode 100644 index 000000000..24f0ae2c4 --- /dev/null +++ b/crypto/ed25519/public_key.go @@ -0,0 +1,146 @@ +package ed25519 + +import ( + "bytes" + "crypto/ed25519" + "io" + + cbor "github.com/fxamacker/cbor/v2" + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/hash" + "github.com/pactus-project/pactus/util/bech32m" + "github.com/pactus-project/pactus/util/encoding" + "github.com/pactus-project/pactus/util/errors" +) + +var _ crypto.PublicKey = &PublicKey{} + +const PublicKeySize = 32 + +type PublicKey struct { + inner ed25519.PublicKey +} + +// PublicKeyFromString decodes the input string and returns the PublicKey +// if the string is a valid bech32m encoding of a BLS public key. +func PublicKeyFromString(text string) (*PublicKey, error) { + // Decode the bech32m encoded public key. + hrp, typ, data, err := bech32m.DecodeToBase256WithTypeNoLimit(text) + if err != nil { + return nil, err + } + + // Check if hrp is valid + if hrp != crypto.PublicKeyHRP { + return nil, crypto.InvalidHRPError(hrp) + } + + if typ != crypto.SignatureTypeEd25519 { + return nil, errors.Errorf(errors.ErrInvalidPublicKey, "invalid public key type: %v", typ) + } + + return PublicKeyFromBytes(data) +} + +// PublicKeyFromBytes constructs a BLS public key from the raw bytes. +func PublicKeyFromBytes(data []byte) (*PublicKey, error) { + if len(data) != PublicKeySize { + return nil, errors.Errorf(errors.ErrInvalidPublicKey, + "public key should be %d bytes, but it is %v bytes", PublicKeySize, len(data)) + } + + return &PublicKey{data}, nil +} + +// Bytes returns the raw byte representation of the public key. +func (pub *PublicKey) Bytes() []byte { + return pub.inner[:PublicKeySize] +} + +// String returns a human-readable string for the BLS public key. +func (pub *PublicKey) String() string { + str, _ := bech32m.EncodeFromBase256WithType( + crypto.PublicKeyHRP, + crypto.SignatureTypeBLS, + pub.Bytes()) + + return str +} + +// MarshalCBOR encodes the public key into CBOR format. +func (pub *PublicKey) MarshalCBOR() ([]byte, error) { + return cbor.Marshal(pub.Bytes()) +} + +// UnmarshalCBOR decodes the public key from CBOR format. +func (pub *PublicKey) UnmarshalCBOR(bs []byte) error { + var data []byte + if err := cbor.Unmarshal(bs, &data); err != nil { + return err + } + + return pub.Decode(bytes.NewReader(data)) +} + +// Encode writes the raw bytes of the public key to the provided writer. +func (pub *PublicKey) Encode(w io.Writer) error { + return encoding.WriteElements(w, pub.Bytes()) +} + +// Decode reads the raw bytes of the public key from the provided reader and initializes the public key. +func (pub *PublicKey) Decode(r io.Reader) error { + data := make([]byte, PublicKeySize) + err := encoding.ReadElements(r, data) + if err != nil { + return err + } + + p, _ := PublicKeyFromBytes(data) + *pub = *p + + return nil +} + +// Verify checks that a signature is valid for the given message and public key. +// It's defined in section 2.6 of the spec: CoreVerify. +func (pub *PublicKey) Verify(msg []byte, sig crypto.Signature) error { + if sig == nil { + return errors.Error(errors.ErrInvalidSignature) + } + + if !ed25519.Verify(pub.inner, msg, sig.Bytes()) { + return crypto.ErrInvalidSignature + } + + return nil +} + +// EqualsTo checks if the current public key is equal to another public key. +func (pub *PublicKey) EqualsTo(x crypto.PublicKey) bool { + xEd25519, ok := x.(*PublicKey) + if !ok { + return false + } + + return pub.inner.Equal(xEd25519.inner) +} + +// AccountAddress returns the account address derived from the public key. +func (pub *PublicKey) AccountAddress() crypto.Address { + data := hash.Hash160(hash.Hash256(pub.Bytes())) + addr := crypto.NewAddress(crypto.AddressTypeBLSAccount, data) + + return addr +} + +// VerifyAddress checks if the provided address matches the derived address from the public key. +func (pub *PublicKey) VerifyAddress(addr crypto.Address) error { + if addr != pub.AccountAddress() { + return crypto.AddressMismatchError{ + Expected: pub.AccountAddress(), + Got: addr, + } + } + + return nil +} diff --git a/crypto/ed25519/public_key_test.go b/crypto/ed25519/public_key_test.go new file mode 100644 index 000000000..2be99a142 --- /dev/null +++ b/crypto/ed25519/public_key_test.go @@ -0,0 +1,233 @@ +package ed25519_test + +import ( + "encoding/hex" + "strings" + "testing" + + cbor "github.com/fxamacker/cbor/v2" + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/util" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPublicKeyCBORMarshaling(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + pub1, _ := ts.RandBLSKeyPair() + pub2 := new(bls.PublicKey) + + bs, err := pub1.MarshalCBOR() + assert.NoError(t, err) + assert.NoError(t, pub2.UnmarshalCBOR(bs)) + assert.True(t, pub1.EqualsTo(pub2)) + + assert.Error(t, pub2.UnmarshalCBOR([]byte("abcd"))) + + inv, _ := hex.DecodeString(strings.Repeat("ff", bls.PublicKeySize)) + data, _ := cbor.Marshal(inv) + assert.NoError(t, pub2.UnmarshalCBOR(data)) + + _, err = pub2.PointG2() + assert.Error(t, err) +} + +func TestPublicKeyEqualsTo(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + pub1, _ := ts.RandBLSKeyPair() + pub2, _ := ts.RandBLSKeyPair() + + assert.True(t, pub1.EqualsTo(pub1)) + assert.False(t, pub1.EqualsTo(pub2)) + assert.Equal(t, pub1, pub1) + assert.NotEqual(t, pub1, pub2) +} + +func TestPublicKeyEncoding(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + pub, _ := ts.RandBLSKeyPair() + w1 := util.NewFixedWriter(20) + assert.Error(t, pub.Encode(w1)) + + w2 := util.NewFixedWriter(bls.PublicKeySize) + assert.NoError(t, pub.Encode(w2)) + + r1 := util.NewFixedReader(20, w2.Bytes()) + assert.Error(t, pub.Decode(r1)) + + r2 := util.NewFixedReader(bls.PublicKeySize, w2.Bytes()) + assert.NoError(t, pub.Decode(r2)) +} + +func TestPublicKeyVerifyAddress(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + pub1, _ := ts.RandBLSKeyPair() + pub2, _ := ts.RandBLSKeyPair() + + err := pub1.VerifyAddress(pub1.AccountAddress()) + assert.NoError(t, err) + err = pub1.VerifyAddress(pub1.ValidatorAddress()) + assert.NoError(t, err) + + err = pub1.VerifyAddress(pub2.AccountAddress()) + assert.Equal(t, crypto.AddressMismatchError{ + Expected: pub1.AccountAddress(), + Got: pub2.AccountAddress(), + }, err) + + err = pub1.VerifyAddress(pub2.ValidatorAddress()) + assert.Equal(t, crypto.AddressMismatchError{ + Expected: pub1.ValidatorAddress(), + Got: pub2.ValidatorAddress(), + }, err) +} + +func TestNilPublicKey(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + pub := &bls.PublicKey{} + randSig := ts.RandBLSSignature() + assert.Error(t, pub.VerifyAddress(ts.RandAccAddress())) + assert.Error(t, pub.VerifyAddress(ts.RandValAddress())) + assert.Error(t, pub.Verify(nil, nil)) + assert.Error(t, pub.Verify(nil, &bls.Signature{})) + assert.Error(t, pub.Verify(nil, randSig)) +} + +func TestNilSignature(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + pub, _ := ts.RandBLSKeyPair() + assert.Error(t, pub.Verify(nil, nil)) + assert.Error(t, pub.Verify(nil, &bls.Signature{})) +} + +func TestPublicKeyBytes(t *testing.T) { + tests := []struct { + errMsg string + encoded string + valid bool + result []byte + }{ + { + "invalid separator index -1", + "not_proper_encoded", + false, nil, + }, + { + "invalid bech32 string length 0", + "", + false, nil, + }, + { + "invalid character not part of charset: 105", + "public1ioiooi", + false, nil, + }, + { + "invalid bech32 string length 0", + "public134jkgz", + false, nil, + }, + { + "invalid HRP: xxx", + "xxx1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjrvqc" + + "vf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5evslaq", + false, nil, + }, + { + "invalid checksum (expected jhx47a got jhx470)", + "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjr" + + "vqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5jhx470", + false, nil, + }, + { + "public key should be 96 bytes, but it is 95 bytes", + "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjr" + + "vqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg73y98kl", + false, nil, + }, + { + "invalid public key type: 2", + "public1z372l5frmm5e7cn7ewfjdkx5t7y62kztqr82rtatat70cl8p8ng3rdzr02mzpwcfl6s2v26kry6mwg" + + "xpqy92ywx9wtff80mc9p3kr4cmhgekj048gavx2zdh78tsnh7eg5jzdw6s3et6c0dqyp22vslcgkukxh4l4", + false, nil, + }, + { + "", + "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjr" + + "vqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5jhx47a", + true, + []byte{ + 0xaf, 0x0f, 0x74, 0x91, 0x7f, 0x50, 0x65, 0xaf, 0x94, 0x72, 0x7a, 0xe9, 0x54, 0x1b, 0x0d, 0xdc, + 0xfb, 0x5b, 0x82, 0x8a, 0x9e, 0x01, 0x6b, 0x02, 0x49, 0x8f, 0x47, 0x7e, 0xd3, 0x7f, 0xb4, 0x4d, 0x5d, + 0x88, 0x24, 0x95, 0xaf, 0xb6, 0xfd, 0x4f, 0x97, 0x73, 0xe4, 0xea, 0x9d, 0xee, 0xe4, 0x36, 0x03, 0x0c, + 0x4d, 0x61, 0xc6, 0xe3, 0xa1, 0x15, 0x15, 0x85, 0xe1, 0xd8, 0x38, 0xca, 0xe1, 0x44, 0x4a, 0x43, 0x8d, + 0x08, 0x9c, 0xe7, 0x7e, 0x10, 0xc4, 0x92, 0xa5, 0x5f, 0x69, 0x08, 0x12, 0x5c, 0x5b, 0xe9, 0xb2, 0x36, + 0xa2, 0x46, 0xe4, 0x08, 0x2d, 0x08, 0xde, 0x56, 0x4e, 0x11, 0x1e, 0x65, + }, + }, + } + + for no, test := range tests { + pub, err := bls.PublicKeyFromString(test.encoded) + if test.valid { + assert.NoError(t, err, "test %v: unexpected error", no) + assert.Equal(t, test.result, pub.Bytes(), "test %v: invalid bytes", no) + assert.Equal(t, test.encoded, pub.String(), "test %v: invalid encoded", no) + } else { + assert.Contains(t, err.Error(), test.errMsg, "test %v: error not matched", no) + } + } +} + +func TestPointG2(t *testing.T) { + tests := []struct { + errMsg string + encoded string + valid bool + }{ + { + "compression flag must be set", + "public1pqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" + + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqjzu9w8", + false, + }, + { + "input string must be zero when infinity flag is set", + "public1pllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll" + + "llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllluhpuzyf", + false, + }, + { + "public key is zero", + "public1pcqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" + + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqglnhh9", + false, + }, + { + "", + "public1p4u8hfytl2pj6l9rj0t54gxcdmna4hq52ncqkkqjf3arha5mlk3x4mzpyjkhmdl20jae7f65aamjr" + + "vqcvf4sudcapz52ctcwc8r9wz3z2gwxs38880cgvfy49ta5ssyjut05myd4zgmjqstggmetyuyg7v5jhx47a", + true, + }, + } + + for no, test := range tests { + pub, err := bls.PublicKeyFromString(test.encoded) + require.NoError(t, err) + + _, err = pub.PointG2() + if test.valid { + assert.NoError(t, err, "test %v: unexpected error", no) + } else { + assert.Contains(t, err.Error(), test.errMsg, "test %v: error not matched", no) + } + } +} diff --git a/crypto/ed25519/signature.go b/crypto/ed25519/signature.go new file mode 100644 index 000000000..98c08d2d6 --- /dev/null +++ b/crypto/ed25519/signature.go @@ -0,0 +1,91 @@ +package ed25519 + +import ( + "bytes" + "crypto/subtle" + "encoding/hex" + "io" + + cbor "github.com/fxamacker/cbor/v2" + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/util/encoding" + "github.com/pactus-project/pactus/util/errors" +) + +var _ crypto.Signature = &Signature{} + +const SignatureSize = 64 + +type Signature struct { + data []byte +} + +// SignatureFromString decodes the input string and returns the Signature +// if the string is a valid hexadecimal encoding of a BLS signature. +func SignatureFromString(text string) (*Signature, error) { + data, err := hex.DecodeString(text) + if err != nil { + return nil, err + } + + return SignatureFromBytes(data) +} + +// SignatureFromBytes constructs a BLS signature from the raw bytes. +func SignatureFromBytes(data []byte) (*Signature, error) { + if len(data) != SignatureSize { + return nil, errors.Errorf(errors.ErrInvalidSignature, + "signature should be %d bytes, but it is %v bytes", SignatureSize, len(data)) + } + + return &Signature{data: data}, nil +} + +// Bytes returns the raw byte representation of the signature. +func (sig *Signature) Bytes() []byte { + return sig.data[:SignatureSize] +} + +// String returns the hex-encoded string representation of the signature. +func (sig *Signature) String() string { + return hex.EncodeToString(sig.Bytes()) +} + +// MarshalCBOR encodes the signature into CBOR format. +func (sig *Signature) MarshalCBOR() ([]byte, error) { + return cbor.Marshal(sig.Bytes()) +} + +// UnmarshalCBOR decodes the signature from CBOR format. +func (sig *Signature) UnmarshalCBOR(bs []byte) error { + var data []byte + if err := cbor.Unmarshal(bs, &data); err != nil { + return err + } + + return sig.Decode(bytes.NewReader(data)) +} + +// Encode writes the raw bytes of the signature to the provided writer. +func (sig *Signature) Encode(w io.Writer) error { + return encoding.WriteElements(w, sig.Bytes()) +} + +// Decode reads the raw bytes of the signature from the provided reader and initializes the signature. +func (sig *Signature) Decode(r io.Reader) error { + data := make([]byte, SignatureSize) + err := encoding.ReadElements(r, data) + if err != nil { + return err + } + + s, _ := SignatureFromBytes(data) + *sig = *s + + return nil +} + +// EqualsTo checks if the current signature is equal to another signature. +func (sig *Signature) EqualsTo(right crypto.Signature) bool { + return subtle.ConstantTimeCompare(sig.data, right.(*Signature).data) == 1 +} diff --git a/crypto/ed25519/signature_test.go b/crypto/ed25519/signature_test.go new file mode 100644 index 000000000..78bc94803 --- /dev/null +++ b/crypto/ed25519/signature_test.go @@ -0,0 +1,178 @@ +package ed25519_test + +import ( + "encoding/hex" + "strings" + "testing" + + cbor "github.com/fxamacker/cbor/v2" + "github.com/pactus-project/pactus/crypto" + "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/util" + "github.com/pactus-project/pactus/util/testsuite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSignatureCBORMarshaling(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + _, prv := ts.RandBLSKeyPair() + sig1 := prv.Sign(ts.RandBytes(16)) + sig2 := new(bls.Signature) + + bs, err := sig1.MarshalCBOR() + assert.NoError(t, err) + assert.NoError(t, sig2.UnmarshalCBOR(bs)) + assert.True(t, sig1.EqualsTo(sig2)) + + assert.Error(t, sig2.UnmarshalCBOR([]byte("abcd"))) + + inv, _ := hex.DecodeString(strings.Repeat("ff", bls.SignatureSize)) + data, _ := cbor.Marshal(inv) + assert.NoError(t, sig2.UnmarshalCBOR(data)) + + _, err = sig2.PointG1() + assert.Error(t, err) +} + +func TestSignatureEqualsTo(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + _, prv := ts.RandBLSKeyPair() + sig1 := prv.Sign([]byte("foo")) + sig2 := prv.Sign([]byte("bar")) + + assert.True(t, sig1.EqualsTo(sig1)) + assert.False(t, sig1.EqualsTo(sig2)) + assert.Equal(t, sig1, sig1) + assert.NotEqual(t, sig1, sig2) +} + +func TestSignatureEncoding(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + _, prv := ts.RandBLSKeyPair() + sig := prv.Sign(ts.RandBytes(16)) + w1 := util.NewFixedWriter(20) + assert.Error(t, sig.Encode(w1)) + + w2 := util.NewFixedWriter(bls.SignatureSize) + assert.NoError(t, sig.Encode(w2)) + + r1 := util.NewFixedReader(20, w2.Bytes()) + assert.Error(t, sig.Decode(r1)) + + r2 := util.NewFixedReader(bls.SignatureSize, w2.Bytes()) + assert.NoError(t, sig.Decode(r2)) +} + +func TestVerifyingSignature(t *testing.T) { + ts := testsuite.NewTestSuite(t) + + msg := []byte("zarb") + + pb1, pv1 := ts.RandBLSKeyPair() + pb2, pv2 := ts.RandBLSKeyPair() + sig1 := pv1.Sign(msg) + sig2 := pv2.Sign(msg) + + assert.False(t, sig1.EqualsTo(sig2)) + assert.NoError(t, pb1.Verify(msg, sig1)) + assert.NoError(t, pb2.Verify(msg, sig2)) + assert.ErrorIs(t, pb1.Verify(msg, sig2), crypto.ErrInvalidSignature) + assert.ErrorIs(t, pb2.Verify(msg, sig1), crypto.ErrInvalidSignature) + assert.ErrorIs(t, pb1.Verify(msg[1:], sig1), crypto.ErrInvalidSignature) +} + +func TestSignatureBytes(t *testing.T) { + tests := []struct { + errMsg string + encoded string + valid bool + bytes []byte + }{ + { + "encoding/hex: invalid byte: U+006E 'n'", + "not_proper_encoded", + false, nil, + }, + { + "signature should be 48 bytes, but it is 0 bytes", + "", + false, nil, + }, + { + "encoding/hex: odd length hex string", + "0", + false, nil, + }, + { + "signature should be 48 bytes, but it is 1 bytes", + "00", + false, nil, + }, + { + "", + "ad0f88cec815e9b8af3f0136297cb242ed8b6369af723fbdac077fa927f5780db7df47c77fb53f3a22324673f000c792", + true, + []byte{ + 0xad, 0x0f, 0x88, 0xce, 0xc8, 0x15, 0xe9, 0xb8, 0xaf, 0x3f, 0x01, 0x36, 0x29, 0x7c, 0xb2, 0x42, + 0xed, 0x8b, 0x63, 0x69, 0xaf, 0x72, 0x3f, 0xbd, 0xac, 0x07, 0x7f, 0xa9, 0x27, 0xf5, 0x78, 0x0d, + 0xb7, 0xdf, 0x47, 0xc7, 0x7f, 0xb5, 0x3f, 0x3a, 0x22, 0x32, 0x46, 0x73, 0xf0, 0x00, 0xc7, 0x92, + }, + }, + } + + for no, test := range tests { + sig, err := bls.SignatureFromString(test.encoded) + if test.valid { + assert.NoError(t, err, "test %v: unexpected error", no) + assert.Equal(t, test.bytes, sig.Bytes(), "test %v: invalid bytes", no) + assert.Equal(t, test.encoded, sig.String(), "test %v: invalid encode", no) + } else { + assert.Contains(t, err.Error(), test.errMsg, "test %v: error not matched", no) + } + } +} + +func TestPointG1(t *testing.T) { + tests := []struct { + errMsg string + encoded string + valid bool + }{ + { + "compression flag must be set", + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + false, + }, + { + "input string must be zero when infinity flag is set", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + false, + }, + { + "signature is zero", + "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + false, + }, + { + "", + "ad0f88cec815e9b8af3f0136297cb242ed8b6369af723fbdac077fa927f5780db7df47c77fb53f3a22324673f000c792", + true, + }, + } + + for no, test := range tests { + sig, err := bls.SignatureFromString(test.encoded) + require.NoError(t, err) + + _, err = sig.PointG1() + if test.valid { + assert.NoError(t, err, "test %v: unexpected error", no) + } else { + assert.Contains(t, err.Error(), test.errMsg, "test %v: error not matched", no) + } + } +} diff --git a/util/testsuite/testsuite.go b/util/testsuite/testsuite.go index b717d9467..728fbbd6f 100644 --- a/util/testsuite/testsuite.go +++ b/util/testsuite/testsuite.go @@ -10,6 +10,7 @@ import ( "github.com/pactus-project/pactus/committee" "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/crypto/bls" + "github.com/pactus-project/pactus/crypto/ed25519" "github.com/pactus-project/pactus/crypto/hash" "github.com/pactus-project/pactus/sortition" "github.com/pactus-project/pactus/types/account" @@ -236,6 +237,19 @@ func (ts *TestSuite) RandBLSKeyPair() (*bls.PublicKey, *bls.PrivateKey) { return pub, prv } +// RandED25519KeyPair generates a random ED25519 key pair for testing purposes. +func (ts *TestSuite) RandED25519KeyPair() (*ed25519.PublicKey, *ed25519.PrivateKey) { + buf := make([]byte, ed25519.PrivateKeySize) + _, err := ts.Rand.Read(buf) + if err != nil { + panic(err) + } + prv, _ := ed25519.PrivateKeyFromBytes(buf) + pub := prv.PublicKeyNative() + + return pub, prv +} + // RandValKey generates a random validator key for testing purposes. func (ts *TestSuite) RandValKey() *bls.ValidatorKey { _, prv := ts.RandBLSKeyPair()