From b91f23c69606b984bf3e81dcff0511a7fab45d17 Mon Sep 17 00:00:00 2001 From: Rongjian Lan Date: Wed, 1 Aug 2018 13:27:02 +0800 Subject: [PATCH] Reuse Dedis lab's kyber library for schnorr multi-sig --- crypto/cosi.go | 399 +++++++++++++++++++++++++++++++++++++++++++ crypto/primitives.go | 109 ------------ crypto/suite.go | 10 ++ 3 files changed, 409 insertions(+), 109 deletions(-) create mode 100644 crypto/cosi.go delete mode 100644 crypto/primitives.go create mode 100644 crypto/suite.go diff --git a/crypto/cosi.go b/crypto/cosi.go new file mode 100644 index 000000000..ab18ca177 --- /dev/null +++ b/crypto/cosi.go @@ -0,0 +1,399 @@ +/* +Package cosi implements the collective signing (CoSi) algorithm as presented in +the paper "Keeping Authorities 'Honest or Bust' with Decentralized Witness +Cosigning" by Ewa Syta et al. See https://arxiv.org/abs/1503.08768. This +package only provides the functionality for the cryptographic operations of +CoSi. All network-related operations have to be handled elsewhere. Below we +describe a high-level overview of the CoSi protocol (using a star communication +topology). We refer to the research paper for further details on communication +over trees, exception mechanisms and signature verification policies. + +The CoSi protocol has four phases executed between a list of participants P +having a protocol leader (index i = 0) and a list of other nodes (index i > 0). +The secret key of node i is denoted by a_i and the public key by A_i = [a_i]G +(where G is the base point of the underlying group and [...] denotes scalar +multiplication). The aggregate public key is given as A = \sum{i ∈ P}(A_i). + +1. Announcement: The leader broadcasts an announcement to the other nodes +optionally including the message M to be signed. Upon receiving an announcement +message, a node starts its commitment phase. + +2. Commitment: Each node i (including the leader) picks a random scalar v_i, +computes its commitment V_i = [v_i]G and sends V_i back to the leader. The +leader waits until it has received enough commitments (according to some +policy) from the other nodes or a timer has run out. Let P' be the nodes that +have sent their commitments. The leader computes an aggregate commitment V from +all commitments he has received, i.e., V = \sum{j ∈ P'}(V_j) and creates a +participation bitmask Z. The leader then broadcasts V and Z to the other +participations together with the message M if it was not sent in phase 1. Upon +receiving a commitment message, a node starts the challenge phase. + +3. Challenge: Each node i computes the collective challenge c = H(V || A || M) +using a cryptographic hash function H (here: SHA512), computes its +response r_i = v_i + c*a_i and sends it back to the leader. + +4. Response: The leader waits until he has received replies from all nodes in +P' or a timer has run out. If he has not enough replies he aborts. Finally, +the leader computes the aggregate response r = \sum{j ∈ P'}(r_j) and publishes +(V,r,Z) as the signature for the message M. +*/ +package crypto + +import ( + "errors" + "fmt" + + "github.com/dedis/kyber" +) + +// Commit returns a random scalar v, generated from the given suite, +// and a corresponding commitment V = [v]G. If the given cipher stream is nil, +// a random stream is used. +func Commit(suite Suite) (v kyber.Scalar, V kyber.Point) { + random := suite.Scalar().Pick(suite.RandomStream()) + commitment := suite.Point().Mul(random, nil) + return random, commitment +} + +// AggregateCommitments returns the sum of the given commitments and the +// bitwise OR of the corresponding masks. +func AggregateCommitments(suite Suite, commitments []kyber.Point, masks [][]byte) (sum kyber.Point, commits []byte, err error) { + if len(commitments) != len(masks) { + return nil, nil, errors.New("mismatching lengths of commitment and mask slices") + } + aggCom := suite.Point().Null() + aggMask := make([]byte, len(masks[0])) + + for i := range commitments { + aggCom = suite.Point().Add(aggCom, commitments[i]) + aggMask, err = AggregateMasks(aggMask, masks[i]) + if err != nil { + return nil, nil, err + } + } + return aggCom, aggMask, nil +} + +// Challenge creates the collective challenge from the given aggregate +// commitment V, aggregate public key A, and message M, i.e., it returns +// c = H(V || A || M). +func Challenge(suite Suite, commitment, public kyber.Point, message []byte) (kyber.Scalar, error) { + if commitment == nil { + return nil, errors.New("no commitment provided") + } + if message == nil { + return nil, errors.New("no message provided") + } + hash := suite.Hash() + if _, err := commitment.MarshalTo(hash); err != nil { + return nil, err + } + if _, err := public.MarshalTo(hash); err != nil { + return nil, err + } + hash.Write(message) + return suite.Scalar().SetBytes(hash.Sum(nil)), nil +} + +// Response creates the response from the given random scalar v, (collective) +// challenge c, and private key a, i.e., it returns r = v + c*a. +func Response(suite Suite, private, random, challenge kyber.Scalar) (kyber.Scalar, error) { + if private == nil { + return nil, errors.New("no private key provided") + } + if random == nil { + return nil, errors.New("no random scalar provided") + } + if challenge == nil { + return nil, errors.New("no challenge provided") + } + ca := suite.Scalar().Mul(private, challenge) + return ca.Add(random, ca), nil +} + +// AggregateResponses returns the sum of given responses. +func AggregateResponses(suite Suite, responses []kyber.Scalar) (kyber.Scalar, error) { + if responses == nil { + return nil, errors.New("no responses provided") + } + r := suite.Scalar().Zero() + for i := range responses { + r = r.Add(r, responses[i]) + } + return r, nil +} + +// Sign returns the collective signature from the given (aggregate) commitment +// V, (aggregate) response r, and participation bitmask Z using the EdDSA +// format, i.e., the signature is V || r || Z. +func Sign(suite Suite, commitment kyber.Point, response kyber.Scalar, mask *Mask) ([]byte, error) { + if commitment == nil { + return nil, errors.New("no commitment provided") + } + if response == nil { + return nil, errors.New("no response provided") + } + if mask == nil { + return nil, errors.New("no mask provided") + } + lenV := suite.PointLen() + lenSig := lenV + suite.ScalarLen() + VB, err := commitment.MarshalBinary() + if err != nil { + return nil, errors.New("marshalling of commitment failed") + } + RB, err := response.MarshalBinary() + if err != nil { + return nil, errors.New("marshalling of signature failed") + } + sig := make([]byte, lenSig+mask.Len()) + copy(sig[:], VB) + copy(sig[lenV:lenSig], RB) + copy(sig[lenSig:], mask.mask) + return sig, nil +} + +// Verify checks the given cosignature on the provided message using the list +// of public keys and cosigning policy. +func Verify(suite Suite, publics []kyber.Point, message, sig []byte, policy Policy) error { + if publics == nil { + return errors.New("no public keys provided") + } + if message == nil { + return errors.New("no message provided") + } + if sig == nil { + return errors.New("no signature provided") + } + if policy == nil { + policy = CompletePolicy{} + } + + lenCom := suite.PointLen() + VBuff := sig[:lenCom] + V := suite.Point() + if err := V.UnmarshalBinary(VBuff); err != nil { + return errors.New("unmarshalling of commitment failed") + } + + // Unpack the aggregate response + lenRes := lenCom + suite.ScalarLen() + rBuff := sig[lenCom:lenRes] + r := suite.Scalar().SetBytes(rBuff) + + // Unpack the participation mask and get the aggregate public key + mask, err := NewMask(suite, publics, nil) + if err != nil { + return err + } + mask.SetMask(sig[lenRes:]) + A := mask.AggregatePublic + ABuff, err := A.MarshalBinary() + if err != nil { + return errors.New("marshalling of aggregate public key failed") + } + + // Recompute the challenge + hash := suite.Hash() + hash.Write(VBuff) + hash.Write(ABuff) + hash.Write(message) + buff := hash.Sum(nil) + k := suite.Scalar().SetBytes(buff) + + // k * -aggPublic + s * B = k*-A + s*B + // from s = k * a + r => s * B = k * a * B + r * B <=> s*B = k*A + r*B + // <=> s*B + k*-A = r*B + minusPublic := suite.Point().Neg(A) + kA := suite.Point().Mul(k, minusPublic) + sB := suite.Point().Mul(r, nil) + left := suite.Point().Add(kA, sB) + + if !left.Equal(V) { + return errors.New("recreated response is different from signature") + } + if !policy.Check(mask) { + return errors.New("the policy is not fulfilled") + } + + return nil +} + +// Mask represents a cosigning participation bitmask. +type Mask struct { + mask []byte + publics []kyber.Point + AggregatePublic kyber.Point +} + +// NewMask returns a new participation bitmask for cosigning where all +// cosigners are disabled by default. If a public key is given it verifies that +// it is present in the list of keys and sets the corresponding index in the +// bitmask to 1 (enabled). +func NewMask(suite Suite, publics []kyber.Point, myKey kyber.Point) (*Mask, error) { + m := &Mask{ + publics: publics, + } + m.mask = make([]byte, m.Len()) + m.AggregatePublic = suite.Point().Null() + if myKey != nil { + found := false + for i, key := range publics { + if key.Equal(myKey) { + m.SetBit(i, true) + found = true + break + } + } + if !found { + return nil, errors.New("key not found") + } + } + return m, nil +} + +// Mask returns a copy of the participation bitmask. +func (m *Mask) Mask() []byte { + clone := make([]byte, len(m.mask)) + copy(clone[:], m.mask) + return clone +} + +// Len returns the mask length in bytes. +func (m *Mask) Len() int { + return (len(m.publics) + 7) >> 3 +} + +// SetMask sets the participation bitmask according to the given byte slice +// interpreted in little-endian order, i.e., bits 0-7 of byte 0 correspond to +// cosigners 0-7, bits 0-7 of byte 1 correspond to cosigners 8-15, etc. +func (m *Mask) SetMask(mask []byte) error { + if m.Len() != len(mask) { + return fmt.Errorf("mismatching mask lengths") + } + for i := range m.publics { + byt := i >> 3 + msk := byte(1) << uint(i&7) + if ((m.mask[byt] & msk) == 0) && ((mask[byt] & msk) != 0) { + m.mask[byt] ^= msk // flip bit in mask from 0 to 1 + m.AggregatePublic.Add(m.AggregatePublic, m.publics[i]) + } + if ((m.mask[byt] & msk) != 0) && ((mask[byt] & msk) == 0) { + m.mask[byt] ^= msk // flip bit in mask from 1 to 0 + m.AggregatePublic.Sub(m.AggregatePublic, m.publics[i]) + } + } + return nil +} + +// SetBit enables (enable: true) or disables (enable: false) the bit +// in the participation mask of the given cosigner. +func (m *Mask) SetBit(i int, enable bool) error { + if i >= len(m.publics) { + return errors.New("index out of range") + } + byt := i >> 3 + msk := byte(1) << uint(i&7) + if ((m.mask[byt] & msk) == 0) && enable { + m.mask[byt] ^= msk // flip bit in mask from 0 to 1 + m.AggregatePublic.Add(m.AggregatePublic, m.publics[i]) + } + if ((m.mask[byt] & msk) != 0) && !enable { + m.mask[byt] ^= msk // flip bit in mask from 1 to 0 + m.AggregatePublic.Sub(m.AggregatePublic, m.publics[i]) + } + return nil +} + +// IndexEnabled checks whether the given index is enabled in the mask or not. +func (m *Mask) IndexEnabled(i int) (bool, error) { + if i >= len(m.publics) { + return false, errors.New("index out of range") + } + byt := i >> 3 + msk := byte(1) << uint(i&7) + return ((m.mask[byt] & msk) != 0), nil +} + +// KeyEnabled checks whether the index, corresponding to the given key, is +// enabled in the mask or not. +func (m *Mask) KeyEnabled(public kyber.Point) (bool, error) { + for i, key := range m.publics { + if key.Equal(public) { + return m.IndexEnabled(i) + } + } + return false, errors.New("key not found") +} + +// CountEnabled returns the number of enabled nodes in the CoSi participation +// mask. +func (m *Mask) CountEnabled() int { + // hw is hamming weight + hw := 0 + for i := range m.publics { + byt := i >> 3 + msk := byte(1) << uint(i&7) + if (m.mask[byt] & msk) != 0 { + hw++ + } + } + return hw +} + +// CountTotal returns the total number of nodes this CoSi instance knows. +func (m *Mask) CountTotal() int { + return len(m.publics) +} + +// AggregateMasks computes the bitwise OR of the two given participation masks. +func AggregateMasks(a, b []byte) ([]byte, error) { + if len(a) != len(b) { + return nil, errors.New("mismatching mask lengths") + } + m := make([]byte, len(a)) + for i := range m { + m[i] = a[i] | b[i] + } + return m, nil +} + +// Policy represents a fully customizable cosigning policy deciding what +// cosigner sets are and aren't sufficient for a collective signature to be +// considered acceptable to a verifier. The Check method may inspect the set of +// participants that cosigned by invoking cosi.Mask and/or cosi.MaskBit, and may +// use any other relevant contextual information (e.g., how security-critical +// the operation relying on the collective signature is) in determining whether +// the collective signature was produced by an acceptable set of cosigners. +type Policy interface { + Check(m *Mask) bool +} + +// CompletePolicy is the default policy requiring that all participants have +// cosigned to make a collective signature valid. +type CompletePolicy struct { +} + +// Check verifies that all participants have contributed to a collective +// signature. +func (p CompletePolicy) Check(m *Mask) bool { + return m.CountEnabled() == m.CountTotal() +} + +// ThresholdPolicy allows to specify a simple t-of-n policy requring that at +// least the given threshold number of participants t have cosigned to make a +// collective signature valid. +type ThresholdPolicy struct { + thold int +} + +// NewThresholdPolicy returns a new ThresholdPolicy with the given threshold. +func NewThresholdPolicy(thold int) *ThresholdPolicy { + return &ThresholdPolicy{thold: thold} +} + +// Check verifies that at least a threshold number of participants have +// contributed to a collective signature. +func (p ThresholdPolicy) Check(m *Mask) bool { + return m.CountEnabled() >= p.thold +} diff --git a/crypto/primitives.go b/crypto/primitives.go deleted file mode 100644 index 67bf65375..000000000 --- a/crypto/primitives.go +++ /dev/null @@ -1,109 +0,0 @@ -package crypto - -import ( - "crypto/cipher" -) - -// A Scalar kyber.y represents a scalar value by which -// a Point (group element) may be encrypted to produce another Point. -// This is an exponent in DSA-style groups, -// in which security is based on the Discrete Logarithm assumption, -// and a scalar multiplier in elliptic curve groups. -type Scalar interface { - // Equality test for two Scalars derived from the same Group - Equal(s2 Scalar) bool - - // Set sets the receiver equal to another Scalar a - Set(a Scalar) Scalar - - // Clone creates a new Scalar with same value - Clone() Scalar - - // Set sets the receiver to a small integer value - SetInt64(v int64) Scalar - - // Set to the additive identity (0) - Zero() Scalar - - // Set to the modular sum of scalars a and b - Add(a, b Scalar) Scalar - - // Set to the modular difference a - b - Sub(a, b Scalar) Scalar - - // Set to the modular negation of scalar a - Neg(a Scalar) Scalar - - // Set to the multiplicative identity (1) - One() Scalar - - // Set to the modular product of scalars a and b - Mul(a, b Scalar) Scalar - - // Set to the modular division of scalar a by scalar b - Div(a, b Scalar) Scalar - - // Set to the modular inverse of scalar a - Inv(a Scalar) Scalar - - // Set to a fresh random or pseudo-random scalar - Pick(rand cipher.Stream) Scalar - - // SetBytes sets the scalar from a byte-slice, - // reducing if necessary to the appropriate modulus. - // The endianess of the byte-slice is determined by the - // implementation. - SetBytes([]byte) Scalar -} - -// A Point kyber.y represents an element of a public-key cryptographic Group. -// For example, -// this is a number modulo the prime P in a DSA-style Schnorr group, -// or an (x, y) point on an elliptic curve. -// A Point can contain a Diffie-Hellman public key, an ElGamal ciphertext, etc. -type Point interface { - // Equality test for two Points derived from the same Group - Equal(s2 Point) bool - - // Null sets the receiver to the neutral identity element. - Null() Point - - // Set sets the receiver to this group's standard base point. - Base() Point - - // Pick sets the receiver to a fresh random or pseudo-random Point. - Pick(rand cipher.Stream) Point - - // Set sets the receiver equal to another Point p. - Set(p Point) Point - - // Clone clones the underlying point. - Clone() Point - - // Maximum number of bytes that can be embedded in a single - // group element via Pick(). - EmbedLen() int - - // Embed encodes a limited amount of specified data in the - // Point, using r as a source of cryptographically secure - // random data. Implementations only embed the first EmbedLen - // bytes of the given data. - Embed(data []byte, r cipher.Stream) Point - - // Extract data embedded in a point chosen via Embed(). - // Returns an error if doesn't represent valid embedded data. - Data() ([]byte, error) - - // Add points so that their scalars add homomorphically - Add(a, b Point) Point - - // Subtract points so that their scalars subtract homomorphically - Sub(a, b Point) Point - - // Set to the negation of point a - Neg(a Point) Point - - // Multiply point p by the scalar s. - // If p == nil, multiply with the standard base point Base(). - Mul(s Scalar, p Point) Point -} diff --git a/crypto/suite.go b/crypto/suite.go new file mode 100644 index 000000000..69f9b0a4c --- /dev/null +++ b/crypto/suite.go @@ -0,0 +1,10 @@ +package crypto + +import "github.com/dedis/kyber" + +// Suite specifies the cryptographic building blocks required for the cosi package. +type Suite interface { + kyber.Group + kyber.HashFactory + kyber.Random +}