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

feat(eddsa): Added EDDSA crypto module #112

Merged
merged 15 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions .vscode/dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ coverprofile
cpuprofile
Debugf
durationpb
eddsa
emptypb
Erevik
eventloop
Expand Down
3 changes: 3 additions & 0 deletions crypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/relab/hotstuff/crypto"
"github.com/relab/hotstuff/crypto/bls12"
"github.com/relab/hotstuff/crypto/ecdsa"
"github.com/relab/hotstuff/crypto/eddsa"
"github.com/relab/hotstuff/internal/testutil"
)

Expand Down Expand Up @@ -167,6 +168,8 @@ func runAll(t *testing.T, run func(*testing.T, setupFunc)) {
t.Helper()
t.Run("Ecdsa", func(t *testing.T) { run(t, setup(NewBase(ecdsa.New), testutil.GenerateECDSAKey)) })
t.Run("Cache+Ecdsa", func(t *testing.T) { run(t, setup(NewCache(ecdsa.New), testutil.GenerateECDSAKey)) })
t.Run("Eddsa", func(t *testing.T) { run(t, setup(NewBase(eddsa.New), testutil.GenerateEDDSAKey)) })
t.Run("Cache+Eddsa", func(t *testing.T) { run(t, setup(NewCache(eddsa.New), testutil.GenerateEDDSAKey)) })
t.Run("BLS12-381", func(t *testing.T) { run(t, setup(NewBase(bls12.New), testutil.GenerateBLS12Key)) })
t.Run("Cache+BLS12-381", func(t *testing.T) { run(t, setup(NewCache(bls12.New), testutil.GenerateBLS12Key)) })
}
Expand Down
93 changes: 13 additions & 80 deletions crypto/ecdsa/ecdsa.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package ecdsa provides a crypto implementation for HotStuff using Go's 'crypto/ecdsa' package.
// Package ecdsa supports spec-k256 curve signature
package ecdsa

import (
Expand All @@ -12,7 +12,6 @@ import (
"github.com/relab/hotstuff/crypto"
"github.com/relab/hotstuff/logging"
"github.com/relab/hotstuff/modules"
"golang.org/x/exp/slices"
)

func init() {
Expand All @@ -27,6 +26,11 @@ const (
PublicKeyFileType = "ECDSA PUBLIC KEY"
)

var (
_ hotstuff.QuorumSignature = (*crypto.MultiSignature[*Signature])(nil)
_ hotstuff.IDSet = (*crypto.MultiSignature[*Signature])(nil)
)

// Signature is an ECDSA signature
type Signature struct {
r, s *big.Int
Expand Down Expand Up @@ -61,79 +65,6 @@ func (sig Signature) ToBytes() []byte {
return b
}

// MultiSignature is a set of (partial) signatures.
type MultiSignature map[hotstuff.ID]*Signature

// RestoreMultiSignature should only be used to restore an existing threshold signature from a set of signatures.
func RestoreMultiSignature(signatures []*Signature) MultiSignature {
sig := make(MultiSignature, len(signatures))
for _, s := range signatures {
sig[s.signer] = s
}
return sig
}

// ToBytes returns the object as bytes.
func (sig MultiSignature) ToBytes() []byte {
var b []byte
// sort by ID to make it deterministic
order := make([]hotstuff.ID, 0, len(sig))
for _, signature := range sig {
order = append(order, signature.signer)
}
slices.Sort(order)
for _, id := range order {
b = append(b, sig[id].ToBytes()...)
}
return b
}

// Participants returns the IDs of replicas who participated in the threshold signature.
func (sig MultiSignature) Participants() hotstuff.IDSet {
return sig
}

// Add adds an ID to the set.
func (sig MultiSignature) Add(_ hotstuff.ID) {
panic("not implemented")
}

// Contains returns true if the set contains the ID.
func (sig MultiSignature) Contains(id hotstuff.ID) bool {
_, ok := sig[id]
return ok
}

// ForEach calls f for each ID in the set.
func (sig MultiSignature) ForEach(f func(hotstuff.ID)) {
for id := range sig {
f(id)
}
}

// RangeWhile calls f for each ID in the set until f returns false.
func (sig MultiSignature) RangeWhile(f func(hotstuff.ID) bool) {
for id := range sig {
if !f(id) {
break
}
}
}

// Len returns the number of entries in the set.
func (sig MultiSignature) Len() int {
return len(sig)
}

func (sig MultiSignature) String() string {
return hotstuff.IDSetToString(sig)
}

var (
_ hotstuff.QuorumSignature = (*MultiSignature)(nil)
_ hotstuff.IDSet = (*MultiSignature)(nil)
)

type ecdsaBase struct {
configuration modules.Configuration
logger logging.Logger
Expand Down Expand Up @@ -166,7 +97,7 @@ func (ec *ecdsaBase) Sign(message []byte) (signature hotstuff.QuorumSignature, e
if err != nil {
return nil, fmt.Errorf("ecdsa: sign failed: %w", err)
}
return MultiSignature{ec.opts.ID(): &Signature{
return crypto.MultiSignature[*Signature]{ec.opts.ID(): &Signature{
r: r,
s: s,
signer: ec.opts.ID(),
Expand All @@ -179,10 +110,10 @@ func (ec *ecdsaBase) Combine(signatures ...hotstuff.QuorumSignature) (hotstuff.Q
return nil, crypto.ErrCombineMultiple
}

ts := make(MultiSignature)
ts := make(crypto.MultiSignature[*Signature])

for _, sig1 := range signatures {
if sig2, ok := sig1.(MultiSignature); ok {
if sig2, ok := sig1.(crypto.MultiSignature[*Signature]); ok {
for id, s := range sig2 {
if _, ok := ts[id]; ok {
return nil, crypto.ErrCombineOverlap
Expand All @@ -199,7 +130,7 @@ func (ec *ecdsaBase) Combine(signatures ...hotstuff.QuorumSignature) (hotstuff.Q

// Verify verifies the given quorum signature against the message.
func (ec *ecdsaBase) Verify(signature hotstuff.QuorumSignature, message []byte) bool {
s, ok := signature.(MultiSignature)
s, ok := signature.(crypto.MultiSignature[*Signature])
if !ok {
ec.logger.Panicf("cannot verify signature of incompatible type %T (expected %T)", signature, s)
}
Expand Down Expand Up @@ -230,7 +161,7 @@ func (ec *ecdsaBase) Verify(signature hotstuff.QuorumSignature, message []byte)

// BatchVerify verifies the given quorum signature against the batch of messages.
func (ec *ecdsaBase) BatchVerify(signature hotstuff.QuorumSignature, batch map[hotstuff.ID][]byte) bool {
s, ok := signature.(MultiSignature)
s, ok := signature.(crypto.MultiSignature[*Signature])
if !ok {
ec.logger.Panicf("cannot verify signature of incompatible type %T (expected %T)", signature, s)
}
Expand Down Expand Up @@ -274,3 +205,5 @@ func (ec *ecdsaBase) verifySingle(sig *Signature, hash hotstuff.Hash) bool {
pk := replica.PublicKey().(*ecdsa.PublicKey)
return ecdsa.Verify(pk, hash[:], sig.R(), sig.S())
}

var _ crypto.Signature = (*Signature)(nil)
184 changes: 184 additions & 0 deletions crypto/eddsa/eddsa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Package eddsa implements 25519 curve signature
package eddsa

import (
"crypto/ed25519"
"crypto/sha256"

"github.com/relab/hotstuff"
"github.com/relab/hotstuff/crypto"
"github.com/relab/hotstuff/logging"
"github.com/relab/hotstuff/modules"
)

func init() {
modules.RegisterModule("eddsa", New)
}

const (
// PrivateKeyFileType is the PEM type for a private key.
PrivateKeyFileType = "EDDSA PRIVATE KEY"

// PublicKeyFileType is the PEM type for a public key.
PublicKeyFileType = "EDDSA PUBLIC KEY"
)

var (
_ hotstuff.QuorumSignature = (*crypto.MultiSignature[*Signature])(nil)
_ hotstuff.IDSet = (*crypto.MultiSignature[*Signature])(nil)
)

// Signature is an ECDSA signature
type Signature struct {
signer hotstuff.ID
sign []byte
}

// RestoreSignature restores an existing signature. It should not be used to create new signatures, use Sign instead.
func RestoreSignature(sign []byte, signer hotstuff.ID) *Signature {
return &Signature{signer, sign}
}

// Signer returns the ID of the replica that generated the signature.
func (sig Signature) Signer() hotstuff.ID {
return sig.signer
}

// ToBytes returns a raw byte string representation of the signature
func (sig Signature) ToBytes() []byte {
var b []byte
b = append(b, sig.sign...)
return b
}

type eddsaBase struct {
configuration modules.Configuration
logger logging.Logger
opts *modules.Options
}

// New returns a new instance of the ECDSA CryptoBase implementation.
func New() modules.CryptoBase {
return &eddsaBase{}
}

// InitModule gives the module a reference to the Core object.
// It also allows the module to set module options using the OptionsBuilder.
func (ed *eddsaBase) InitModule(mods *modules.Core) {
mods.Get(
&ed.configuration,
&ed.logger,
&ed.opts,
)
}

func (ed *eddsaBase) privateKey() ed25519.PrivateKey {
return ed.opts.PrivateKey().(ed25519.PrivateKey)
}

func (ed *eddsaBase) Sign(message []byte) (signature hotstuff.QuorumSignature, err error) {
sign := ed25519.Sign(ed.privateKey(), message)
eddsaSign := &Signature{signer: ed.opts.ID(), sign: sign}
return crypto.MultiSignature[*Signature]{ed.opts.ID(): eddsaSign}, nil
}

func (ed *eddsaBase) Combine(signatures ...hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) {
if len(signatures) < 2 {
return nil, crypto.ErrCombineMultiple
}

Check warning on line 88 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L87-L88

Added lines #L87 - L88 were not covered by tests
meling marked this conversation as resolved.
Show resolved Hide resolved

ts := make(crypto.MultiSignature[*Signature])

for _, sig1 := range signatures {
if sig2, ok := sig1.(crypto.MultiSignature[*Signature]); ok {
for id, s := range sig2 {
if _, ok := ts[id]; ok {
return nil, crypto.ErrCombineOverlap
}

Check warning on line 97 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L96-L97

Added lines #L96 - L97 were not covered by tests
ts[id] = s
}
} else {
ed.logger.Panicf("cannot combine signature of incompatible type %T (expected %T)", sig1, sig2)
}

Check warning on line 102 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L100-L102

Added lines #L100 - L102 were not covered by tests
}
return ts, nil
}

func (ed *eddsaBase) Verify(signature hotstuff.QuorumSignature, message []byte) bool {
s, ok := signature.(crypto.MultiSignature[*Signature])
if !ok {
ed.logger.Panicf("cannot verify signature of incompatible type %T (expected %T)", signature, s)
}

Check warning on line 111 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L110-L111

Added lines #L110 - L111 were not covered by tests
n := signature.Participants().Len()
if n == 0 {
return false
}

Check warning on line 115 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L114-L115

Added lines #L114 - L115 were not covered by tests

results := make(chan bool, n)

for _, sig := range s {
go func(sig *Signature, msg []byte) {
results <- ed.verifySingle(sig, msg)
}(sig, message)
}

valid := true
for range s {
if !<-results {
valid = false
}

Check warning on line 129 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L128-L129

Added lines #L128 - L129 were not covered by tests
}

return valid

}
func (ed *eddsaBase) BatchVerify(signature hotstuff.QuorumSignature, batch map[hotstuff.ID][]byte) bool {
s, ok := signature.(crypto.MultiSignature[*Signature])
if !ok {
ed.logger.Panicf("cannot verify signature of incompatible type %T (expected %T)", signature, s)
}

Check warning on line 139 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L138-L139

Added lines #L138 - L139 were not covered by tests

n := signature.Participants().Len()
if n == 0 {
return false
}

Check warning on line 144 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L143-L144

Added lines #L143 - L144 were not covered by tests

results := make(chan bool, n)
set := make(map[hotstuff.Hash]struct{})
for id, sig := range s {
message, ok := batch[id]
if !ok {
return false
}

Check warning on line 152 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L151-L152

Added lines #L151 - L152 were not covered by tests
hash := sha256.Sum256(message)
set[hash] = struct{}{}
go func(sig *Signature, msg []byte) {
results <- ed.verifySingle(sig, msg)
}(sig, message)
}

valid := true
for range s {
if !<-results {
valid = false
}

Check warning on line 164 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L163-L164

Added lines #L163 - L164 were not covered by tests
}

// valid if all partial signatures are valid and there are no duplicate messages
return valid && len(set) == len(batch)
}
func (ed *eddsaBase) verifySingle(sig *Signature, message []byte) bool {
replica, ok := ed.configuration.Replica(sig.Signer())
if !ok {
ed.logger.Warnf("ecdsaBase: got signature from replica whose ID (%d) was not in the config.", sig.Signer())
return false
}

Check warning on line 175 in crypto/eddsa/eddsa.go

View check run for this annotation

Codecov / codecov/patch

crypto/eddsa/eddsa.go#L173-L175

Added lines #L173 - L175 were not covered by tests
pk, ok := replica.PublicKey().([]byte)
if !ok {
ed.logger.Infof("ecdsaBase: got public key from replica that was not of type []byte.")
pk = replica.PublicKey().(ed25519.PublicKey)
}
return ed25519.Verify(pk, message, sig.sign)
}

var _ crypto.Signature = (*Signature)(nil)
Loading
Loading