mirror of
https://github.com/luxfi/lattice.git
synced 2026-09-24 00:25:44 +00:00
* feat(multiparty): serialization for ShamirPolynomial + Combiner active-set validation Closes the only multiparty/ type missing the serialization contract documented in the package README (ShamirPolynomial), enabling parties to persist their threshold-share generator polynomial across protocol sessions (proactive re-sharing, crash recovery, offline distribution). Methods delegate to the underlying structs.Vector[ringqp.Poly] to stay byte-equivalent with existing share types. Also hardens Combiner.GenAdditiveShare: when an activesPoints entry isn't present in the others set passed to NewCombiner, the current code panics inside MulRNSScalar due to a nil RNSScalar map miss; this returns a typed error instead. Adds round-trip serialization and negative-path tests under testThreshold. * chore: gofmt 4 files flagged by static checks Pure formatting (operator spacing, alignment) on: - gpu/gpu.go - gpu/gpu_test.go - types/reduction_test.go - types/trust_test.go These were tripping `make checks` (`go fmt: problems in files`) on this PR's static-check workflow. No semantic changes. --------- Co-authored-by: Abhishek Krishna <[email protected]>
290 lines
12 KiB
Go
290 lines
12 KiB
Go
package multiparty
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/luxfi/lattice/v7/core/rlwe"
|
|
"github.com/luxfi/lattice/v7/ring"
|
|
"github.com/luxfi/lattice/v7/ring/ringqp"
|
|
"github.com/luxfi/lattice/v7/utils/sampling"
|
|
"github.com/luxfi/lattice/v7/utils/structs"
|
|
)
|
|
|
|
// Thresholdizer is a type for generating secret-shares of [ringqp.Poly] types such that
|
|
// the resulting sharing has a t-out-of-N-threshold access-structure. It implements the
|
|
// "Thresholdize" operation as presented in "An Efficient Threshold Access-Structure
|
|
// for RLWE-Based Multiparty Homomorphic Encryption" (2022) by Mouchet, C., Bertrand, E.,
|
|
// and Hubaux, J. P. (https://eprint.iacr.org/2022/780).
|
|
//
|
|
// See the [multiparty] package README.md.
|
|
type Thresholdizer struct {
|
|
params *rlwe.Parameters
|
|
ringQP *ringqp.Ring
|
|
usampler ringqp.UniformSampler
|
|
}
|
|
|
|
// Combiner is a type for generating t-out-of-t additive shares from local t-out-of-N
|
|
// shares. It implements the "Combine" operation as presented in "An Efficient Threshold
|
|
// Access-Structure for RLWE-Based Multiparty Homomorphic Encryption" (2022) by Mouchet, C.,
|
|
// Bertrand, E., and Hubaux, J. P. (https://eprint.iacr.org/2022/780).
|
|
type Combiner struct {
|
|
ringQP *ringqp.Ring
|
|
threshold int
|
|
tmp1, tmp2 []uint64
|
|
one ring.RNSScalar
|
|
lagrangeCoeffs map[ShamirPublicPoint]ring.RNSScalar
|
|
}
|
|
|
|
// ShamirPublicPoint is a type for Shamir public point associated with a party identity within
|
|
// the t-out-of-N-threshold scheme.
|
|
//
|
|
// See [Thresholdizer] and [Combiner] types.
|
|
type ShamirPublicPoint uint64
|
|
|
|
// ShamirPolynomial represents a polynomial with [ringqp.Poly] coefficients. It is used by the
|
|
// Thresholdizer type to produce t-out-of-N-threshold shares of an [ringqp.Poly].
|
|
//
|
|
// See [Thresholdizer] type.
|
|
type ShamirPolynomial struct {
|
|
Value structs.Vector[ringqp.Poly]
|
|
}
|
|
|
|
// ShamirSecretShare represents a t-out-of-N-threshold secret-share.
|
|
//
|
|
// See [Thresholdizer] and [Combiner] types.
|
|
type ShamirSecretShare struct {
|
|
ringqp.Poly
|
|
}
|
|
|
|
// NewThresholdizer creates a new [Thresholdizer] instance from parameters.
|
|
func NewThresholdizer(params rlwe.ParameterProvider) Thresholdizer {
|
|
|
|
thr := Thresholdizer{}
|
|
thr.params = params.GetRLWEParameters()
|
|
thr.ringQP = thr.params.RingQP()
|
|
|
|
prng, err := sampling.NewPRNG()
|
|
|
|
// Sanity check, this error should not happen.
|
|
if err != nil {
|
|
panic(fmt.Errorf("could not initialize PRNG: %s", err))
|
|
}
|
|
|
|
thr.usampler = ringqp.NewUniformSampler(prng, *thr.params.RingQP())
|
|
|
|
return thr
|
|
}
|
|
|
|
// GenShamirPolynomial generates a new secret [ShamirPolynomial] to be used in the [Thresholdizer.GenShamirSecretShare] method.
|
|
// It does so by sampling a random polynomial of degree threshold - 1 and with its constant term equal to secret.
|
|
func (thr Thresholdizer) GenShamirPolynomial(threshold int, secret *rlwe.SecretKey) (ShamirPolynomial, error) {
|
|
if threshold < 1 {
|
|
return ShamirPolynomial{}, fmt.Errorf("threshold should be >= 1")
|
|
}
|
|
gen := make([]ringqp.Poly, int(threshold))
|
|
gen[0] = *secret.Value.CopyNew()
|
|
for i := 1; i < threshold; i++ {
|
|
gen[i] = thr.ringQP.NewPoly()
|
|
thr.usampler.Read(gen[i])
|
|
}
|
|
|
|
return ShamirPolynomial{Value: structs.Vector[ringqp.Poly](gen)}, nil
|
|
}
|
|
|
|
// AllocateThresholdSecretShare allocates a [ShamirSecretShare] struct.
|
|
func (thr Thresholdizer) AllocateThresholdSecretShare() ShamirSecretShare {
|
|
return ShamirSecretShare{thr.ringQP.NewPoly()}
|
|
}
|
|
|
|
// GenShamirSecretShare generates a secret share for the given recipient, identified by its [ShamirPublicPoint].
|
|
// The result is stored in ShareOut and should be sent to this party.
|
|
func (thr Thresholdizer) GenShamirSecretShare(recipient ShamirPublicPoint, secretPoly ShamirPolynomial, shareOut *ShamirSecretShare) {
|
|
thr.ringQP.EvalPolyScalar(secretPoly.Value, uint64(recipient), shareOut.Poly)
|
|
}
|
|
|
|
// AggregateShares aggregates two [ShamirSecretShare] and stores the result in outShare.
|
|
func (thr Thresholdizer) AggregateShares(share1, share2 ShamirSecretShare, outShare *ShamirSecretShare) (err error) {
|
|
if share1.LevelQ() != share2.LevelQ() || share1.LevelQ() != outShare.LevelQ() || share1.LevelP() != share2.LevelP() || share1.LevelP() != outShare.LevelP() {
|
|
return fmt.Errorf("cannot AggregateShares: shares level do not match")
|
|
}
|
|
thr.ringQP.AtLevel(share1.LevelQ(), share1.LevelP()).Add(share1.Poly, share2.Poly, outShare.Poly)
|
|
return
|
|
}
|
|
|
|
// NewCombiner creates a new [Combiner] struct from the parameters and the set of [ShamirPublicPoints]. Note that the other
|
|
// parameter may contain the instantiator's own [ShamirPublicPoint].
|
|
func NewCombiner(params rlwe.ParameterProvider, own ShamirPublicPoint, others []ShamirPublicPoint, threshold int) Combiner {
|
|
cmb := Combiner{}
|
|
cmb.ringQP = params.GetRLWEParameters().RingQP()
|
|
cmb.threshold = threshold
|
|
cmb.tmp1, cmb.tmp2 = cmb.ringQP.NewRNSScalar(), cmb.ringQP.NewRNSScalar()
|
|
cmb.one = cmb.ringQP.NewRNSScalarFromUInt64(1)
|
|
|
|
qlen := cmb.ringQP.RingQ.ModuliChainLength()
|
|
for i, s := range cmb.ringQP.RingQ.SubRings {
|
|
cmb.one[i] = ring.MForm(cmb.one[i], s.Modulus, s.BRedConstant)
|
|
}
|
|
if cmb.ringQP.RingP != nil {
|
|
for i, s := range cmb.ringQP.RingP.SubRings {
|
|
cmb.one[i+qlen] = ring.MForm(cmb.one[i+qlen], s.Modulus, s.BRedConstant)
|
|
}
|
|
}
|
|
|
|
// precomputes lagrange coefficient factors
|
|
cmb.lagrangeCoeffs = make(map[ShamirPublicPoint]ring.RNSScalar)
|
|
for _, spk := range others {
|
|
if spk != own {
|
|
cmb.lagrangeCoeffs[spk] = cmb.ringQP.NewRNSScalar()
|
|
cmb.lagrangeCoeff(own, spk, cmb.lagrangeCoeffs[spk])
|
|
}
|
|
}
|
|
|
|
return cmb
|
|
}
|
|
|
|
// GenAdditiveShare generates a t-out-of-t additive share of the secret from a local aggregated share ownSecret and the set of active identities, identified
|
|
// by their [ShamirPublicPoint]. It stores the resulting additive share in skOut.
|
|
func (cmb Combiner) GenAdditiveShare(activesPoints []ShamirPublicPoint, ownPoint ShamirPublicPoint, ownShare ShamirSecretShare, skOut *rlwe.SecretKey) (err error) {
|
|
|
|
if len(activesPoints) < cmb.threshold {
|
|
return fmt.Errorf("cannot GenAdditiveShare: Not enough active players to combine threshold shares")
|
|
}
|
|
|
|
// Validate that every active party (other than self) has a precomputed Lagrange
|
|
// coefficient. Without this check, a stale or churned active-set would silently
|
|
// pass `nil` into MulRNSScalar and panic deep in scalar arithmetic.
|
|
for _, active := range activesPoints[:cmb.threshold] {
|
|
if active == ownPoint {
|
|
continue
|
|
}
|
|
if _, ok := cmb.lagrangeCoeffs[active]; !ok {
|
|
return fmt.Errorf("cannot GenAdditiveShare: active party %d not in the Combiner's others set", active)
|
|
}
|
|
}
|
|
|
|
prod := cmb.tmp2
|
|
copy(prod, cmb.one)
|
|
|
|
for _, active := range activesPoints[:cmb.threshold] {
|
|
//Lagrange Interpolation with the public threshold key of other active players
|
|
if active != ownPoint {
|
|
cmb.tmp1 = cmb.lagrangeCoeffs[active]
|
|
cmb.ringQP.MulRNSScalar(prod, cmb.tmp1, prod)
|
|
}
|
|
}
|
|
|
|
cmb.ringQP.MulRNSScalarMontgomery(ownShare.Poly, prod, skOut.Value)
|
|
return
|
|
}
|
|
|
|
func (cmb Combiner) lagrangeCoeff(thisKey ShamirPublicPoint, thatKey ShamirPublicPoint, lagCoeff []uint64) {
|
|
|
|
this := cmb.ringQP.NewRNSScalarFromUInt64(uint64(thisKey))
|
|
that := cmb.ringQP.NewRNSScalarFromUInt64(uint64(thatKey))
|
|
|
|
cmb.ringQP.SubRNSScalar(that, this, lagCoeff)
|
|
|
|
cmb.ringQP.Inverse(lagCoeff)
|
|
|
|
cmb.ringQP.MulRNSScalar(lagCoeff, that, lagCoeff)
|
|
}
|
|
|
|
// BinarySize returns the serialized size of the object in bytes.
|
|
func (s ShamirSecretShare) BinarySize() int {
|
|
return s.Poly.BinarySize()
|
|
}
|
|
|
|
// WriteTo writes the object on an [io.Writer]. It implements the [io.WriterTo]
|
|
// interface, and will write exactly object.BinarySize() bytes on w.
|
|
//
|
|
// Unless w implements the [buffer.Writer] interface (see lattice/utils/buffer/writer.go),
|
|
// it will be wrapped into a [bufio.Writer]. Since this requires allocations, it
|
|
// is preferable to pass a [buffer.Writer] directly:
|
|
//
|
|
// - When writing multiple times to a [io.Writer], it is preferable to first wrap the
|
|
// [io.Writer] in a pre-allocated [bufio.Writer].
|
|
// - When writing to a pre-allocated var b []byte, it is preferable to pass
|
|
// buffer.NewBuffer(b) as w (see lattice/utils/buffer/buffer.go).
|
|
func (s ShamirSecretShare) WriteTo(w io.Writer) (n int64, err error) {
|
|
return s.Poly.WriteTo(w)
|
|
}
|
|
|
|
// ReadFrom reads on the object from an [io.Writer]. It implements the
|
|
// [io.ReaderFrom] interface.
|
|
//
|
|
// Unless r implements the [buffer.Reader] interface (see see lattice/utils/buffer/reader.go),
|
|
// it will be wrapped into a [bufio.Reader]. Since this requires allocation, it
|
|
// is preferable to pass a [buffer.Reader] directly:
|
|
//
|
|
// - When reading multiple values from a [io.Reader], it is preferable to first
|
|
// first wrap [io.Reader] in a pre-allocated [bufio.Reader].
|
|
// - When reading from a var b []byte, it is preferable to pass a buffer.NewBuffer(b)
|
|
// as w (see lattice/utils/buffer/buffer.go).
|
|
func (s *ShamirSecretShare) ReadFrom(r io.Reader) (n int64, err error) {
|
|
return s.Poly.ReadFrom(r)
|
|
}
|
|
|
|
// MarshalBinary encodes the object into a binary form on a newly allocated slice of bytes.
|
|
func (s ShamirSecretShare) MarshalBinary() (p []byte, err error) {
|
|
return s.Poly.MarshalBinary()
|
|
}
|
|
|
|
// UnmarshalBinary decodes a slice of bytes generated by
|
|
// [ShamirSecretShare.MarshalBinary] or [ShamirSecretShare.WriteTo] on the object.
|
|
func (s *ShamirSecretShare) UnmarshalBinary(p []byte) (err error) {
|
|
return s.Poly.UnmarshalBinary(p)
|
|
}
|
|
|
|
// BinarySize returns the serialized size of the object in bytes.
|
|
func (s ShamirPolynomial) BinarySize() int {
|
|
return s.Value.BinarySize()
|
|
}
|
|
|
|
// WriteTo writes the object on an [io.Writer]. It implements the [io.WriterTo]
|
|
// interface, and will write exactly object.BinarySize() bytes on w.
|
|
//
|
|
// Security: a [ShamirPolynomial] holds the generator polynomial whose constant
|
|
// term is the dealer's secret. The marshalled bytes MUST be encrypted at rest
|
|
// and MUST NOT be transmitted over a non-private channel.
|
|
//
|
|
// Unless w implements the [buffer.Writer] interface (see lattice/utils/buffer/writer.go),
|
|
// it will be wrapped into a [bufio.Writer]. Since this requires allocations, it
|
|
// is preferable to pass a [buffer.Writer] directly:
|
|
//
|
|
// - When writing multiple times to a [io.Writer], it is preferable to first wrap the
|
|
// [io.Writer] in a pre-allocated [bufio.Writer].
|
|
// - When writing to a pre-allocated var b []byte, it is preferable to pass
|
|
// buffer.NewBuffer(b) as w (see lattice/utils/buffer/buffer.go).
|
|
func (s ShamirPolynomial) WriteTo(w io.Writer) (n int64, err error) {
|
|
return s.Value.WriteTo(w)
|
|
}
|
|
|
|
// ReadFrom reads on the object from an [io.Reader]. It implements the
|
|
// [io.ReaderFrom] interface.
|
|
//
|
|
// Unless r implements the [buffer.Reader] interface (see lattice/utils/buffer/reader.go),
|
|
// it will be wrapped into a [bufio.Reader]. Since this requires allocation, it
|
|
// is preferable to pass a [buffer.Reader] directly:
|
|
//
|
|
// - When reading multiple values from a [io.Reader], it is preferable to first
|
|
// first wrap [io.Reader] in a pre-allocated [bufio.Reader].
|
|
// - When reading from a var b []byte, it is preferable to pass a buffer.NewBuffer(b)
|
|
// as w (see lattice/utils/buffer/buffer.go).
|
|
func (s *ShamirPolynomial) ReadFrom(r io.Reader) (n int64, err error) {
|
|
return s.Value.ReadFrom(r)
|
|
}
|
|
|
|
// MarshalBinary encodes the object into a binary form on a newly allocated slice
|
|
// of bytes. See [ShamirPolynomial.WriteTo] for the security caveats — this
|
|
// output reveals the dealer's secret and MUST be protected accordingly.
|
|
func (s ShamirPolynomial) MarshalBinary() (p []byte, err error) {
|
|
return s.Value.MarshalBinary()
|
|
}
|
|
|
|
// UnmarshalBinary decodes a slice of bytes generated by
|
|
// [ShamirPolynomial.MarshalBinary] or [ShamirPolynomial.WriteTo] on the object.
|
|
func (s *ShamirPolynomial) UnmarshalBinary(p []byte) (err error) {
|
|
return s.Value.UnmarshalBinary(p)
|
|
}
|