From 5844ab46a71738b49b4a68973688b86de4dd4046 Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Tue, 10 Dec 2024 18:39:56 +0100 Subject: [PATCH 01/21] [readme] first draft of improved README.md for multiparty schemes --- multiparty/README.md | 90 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/multiparty/README.md b/multiparty/README.md index a25d2f18..1d1541e4 100644 --- a/multiparty/README.md +++ b/multiparty/README.md @@ -1,14 +1,92 @@ -# Multi-Party +# Multiparty Schemes in Lattigo The `multiparty` package implements several Multiparty Homomorphic Encryption (MHE) primitives based on Ring-Learning-with-Errors (RLWE). -It provides generic interfaces for the local steps of the MHE-based Secure Multiparty Computation (MHE-MPC) protocol that are common across all the RLWE distributed schemes implemented in Lattigo (e.g., collective key generation). -The `multiparty/mpbgv` and `multiparty/mpckks` packages provide scheme-specific functionalities (e.g., interactive bootstrapping) by implementing **threshold** versions of the single-party BFV/BGV and CKKS cryptosystems +It provides the implementation of two core schemes: + +1. A $N$-out-of-$N$-threshold scheme +2. A $t$-out-of-$N$-threshold scheme + +We provide more informations about these two core schemes below. Moreover, The `multiparty/mpbgv` and `multiparty/mpckks` packages provide scheme-specific functionalities (e.g., interactive bootstrapping) by implementing **threshold** versions of the single-party BFV/BGV and CKKS cryptosystems found in the `schemes` package. +Note that, as for the single party schemes, most of the operations are generic and can be handled by the core schemes in the `multiparty` package. -This package implements local operations only, hence does not assume or provide any network-layer protocol implementation. -However, it provides serialization methods for all relevant structures that implement the standard `encoding.BinaryMarshaller` and `encoding.BinaryUnmarshaller` interfaces (see [https://pkg.go.dev/encoding](https://pkg.go.dev/encoding)) as well as the `io.WriterTo` and `io.ReaderFrom` interfaces (see [https://pkg.go.dev/encoding](https://pkg.go.dev/io)). +The `multiparty` package and its sub-packages provide the **local steps** of the MHE-based Secure Multiparty Computation protocol (MHE-MPC), as described in ["Multiparty Homomorphic Encryption from Ring-Learning-with-Errors"](https://eprint.iacr.org/2020/304.pdf) by Mouchet et al. (2021) (which is an RLWE instantiation of the MPC protocol described in ["Multiparty computation with low communication, computation and interaction via threshold FHE"](https://eprint.iacr.org/2011/613.pdf) by Asharov et al. (2012)). -The MHE-MPC protocol implemented in Lattigo is based on the constructions described in ["Multiparty Homomorphic Encryption from Ring-Learning-with-Errors"](https://eprint.iacr.org/2020/304.pdf) by Mouchet et al. (2021), which is an RLWE instantiation of the MPC protocol described in ["Multiparty computation with low communication, computation and interaction via threshold FHE"](https://eprint.iacr.org/2011/613.pdf) by Asharov et al. (2012). +Note that this package implements local operations only, hence does not assume or provide any network-layer protocol implementation. +However: +- All relevant types provide serialization methods implementing the `encoding.BinaryMarshaller` and `encoding.BinaryUnmarshaller` interfaces (see [https://pkg.go.dev/encoding](https://pkg.go.dev/encoding)) as well as the `io.WriterTo` and `io.ReaderFrom` interfaces (see [https://pkg.go.dev/io](https://pkg.go.dev/io)). +- The last section of this README provides a detailed overview of the MHE-MPC protocol, its different instantiations, and maps the protocol steps to the relevant Lattigo types and methods. +- The `examples/multiparty` folder contains example applications for simple MPC tasks. These examples are running all the parties in the same process, but demonstrate the use of the multiparty schemes in the MHE-MPC protocol. + +## The $N$-out-of-$N$-Threshold Scheme + +Conceptually, the $N$-out-of-$N$-threshold scheme exploits the linearity of RLWE encryption to distribute the secret-key among $N$ parties. More specifically, the core cryptographic operation of (single-party) RLWE-based scheme is to compute functions of the form: $$F(a,s) = as+e$$ +over a ring $R$ where $a \in R$ is public, $s \in R$ is the secret-key of the scheme and $e \in R$ is a small ring element (sampled fresh for each function). For example, notice that generating an RLWE public-key corresponds to exactly this operation. The $N$-out-of-$N$-threshold scheme consists in splitting the secret-key $s$ into $N$ additive shares such that $s=\sum^N_{i=1} s_i$ and that $s_i$ is held by party $i$. +In this way, any secret-key operation (especially, decryption) requires the collaboration between **all** the $N$ parties. + +Exploiting the linear structure of $s$ and the (almost) linearity of $F$, the latter can be computed piece-wise by the parties, as: + +$$h_i = F(a, s_i) = as_i + e_i.$$ + +Then $F(a,s)$ can be computed as $h=\sum^N_{i=1}h_i$. The output $h$ is the desired function, only with larger error. Moreover, the following features are relevant: + +- Since $F(a, s_i)$ has the form of a RLWE sample, it can be publicly disclosed for aggregation. For example, the parties could use a helper for aggregating their shares $h_i$. +- Since the secret-key shares $s_i$ are random and never disclosed, they can be sampled locally by the parties (as normal RLWE secret-keys). Hence, no trusted dealer or private channels are necessary between the parties. +- Obtaining protocols for threshold generation of evaluation-keys and threshold decryption only requires to slightly adapt the function $F$ above. The overall linearity argument remains the same. +- The threshold decryption protocol can be generalized into a re-encryption protocol (where the actual decryption corresponds to re-encrypting towards the $s=0$ key). It is possible to re-encrypt towards a known public-key (for receivers external to the set of parties). +- The threshold decryption- and key-switching protocols require *smudging* parameter implementing the noise-flooding technique. This is a statistical security parameter ensuring that no secret value leaks to the decryption receiver through the error. See the SECURITY.md for more discussion on this aspect. + +### Implementation + +For each secret-key operation in the single party RLWE scheme, the `multiparty` package provides a "protocol" type implementing the local operations of the parties in the threshold scheme. More specifically, each protocol type provides a `GenShare(*rlwe.SecretKey, [...])` method for generating a party's share (i.e., computing $F(a, s_i)$ above) and a `AggregateShares(share1, share2 [...])` method to aggregate the shares. Moreover: + +- For some protocols, $a$ is a *common random polynomial* (CRP) sampled from a common random string (CRS). In this case, the protocol types provide a `SampleCRP(crs CRS)` method to obtain $a$. +- The protocol types also provide method for converting the final aggregated share into the protocol's output. E.g., the `PublicKeyGenProtocol` provides a `GenPublicKey(aggshare PublicKeyGenShare, [...])` method. + +## The $t$-out-of-$N$-Threshold Scheme + +There might be settings where an $N$-out-of-$N$-threshold access-structure is too restrictive. For example, when $N$ is large, the probability of a single party being down at a given time increases. In cases where it can be assumed that the adversary cannot corrupt more than $t-1$ out of the $N$ parties, the $t$-out-of-$N$-threshold scheme can be employed to provide better liveness guarantees. More specifically, this scheme ensures that secret-key operations can be performed by any group of at least $t$ parties. + +Lattigo provides an implementation of the RLWE-based $t$-out-of-$N$-threshold scheme described in Mouchet et al.'s paper [An Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic Encryption](https://eprint.iacr.org/2022/780). Similarly to many threshold schemes, it relies on Shamir Secret Sharing to distribute the secret-key of the scheme. This is, the secret-key of the scheme is now of the form: + +$$S(X) = s + s_1 X + s_2 X^2 + ... + s_t X^{t-1},$$ + +i.e., a degree-$(t-1)$ polynomial in R[X] for which $s = S(0)$, and party $i$'s secret-key shares is distributed as $S(\alpha_i)$ for $(\alpha_1, \alpha_2, ... \alpha_N)$ $N$ distinct elements of $R$ forming an exceptional sequence. +Then, observe that $s$ can be reconstructed from any set of $t$ shares via Lagrange interpolation. For example, assuming reconstruction from the first $t$ shares: + +$$s = \sum^t_{i=1} S(\alpha_i) \cdot \prod^t_{\substack{j=1\\ i \neq j}} \frac{\alpha_j}{\alpha_j - \alpha_i} = \sum^t_{i=1} S(\alpha_i) \cdot l_i$$ + +Hence, the structure of $s$ is still linear, and we can again compute the $F$ function above piece-wise. However, the fact that $F(a,s)$ is only **approximately** linear in $s$ poses some challenge in performing the Shamir reconstruction in the "usual" way. +More specifically, we cannot compute $h_i = F(a, S(\alpha_i))$ locally and then combine the shares as $\sum^t_{i=1} h_i \cdot l_i$. This is because $l_i$ is a large $R$ element multiplying it with $S(0)a+e_i$ would result in a large error $e_i \cdot l_i$. + +The scheme of Mouchet et al. circumvents this issue by directly evaluating $h_i=F(a, S(\alpha_i) \cdot l_i)$ locally. Then the combination of the share is back to being a simple summation over $t$ shares: $h =\sum^t_{i=1} h_i$. This simple trick enables a very efficient and usable $t$-out-of-$N$ scheme: + +- $S$ can be generated non-interactively and without a trusted dealer by having each party generating a random degree-$(t-1)$ polynomial $S_i$ with $S_i(0) = s_i$, and by implicitly take $S=\sum^N_{i=1} S_i$. Observe then that $s = S(0) = \sum^N_{i=1} s_i$, which matches the $N$-out-of-$N$-threshold case. +- Then, party $i$ can obtain its share $S(\alpha_i)$ by: + 1. having each party $j$ send $S_j(\alpha_i)$ to party $i$ (via a **private** channel), + 2. having party $i$ compute $S(\alpha_i) = \sum^N_{j=1} S_j(\alpha_i)$. +- The above protocol is a single-round protocol, and state each party has to keep is then a single ring element $S(\alpha_i)$. +- When instantiated as above, the $t$-out-of-$N$-threshold scheme consists in a direct **extension** of the $N$-out-of-$N$-threshold scheme where: + 1. The parties operate a *re-sharing* of their secret-key $N$-out-of-$N$ secret-key share using the $t$-out-of-$N$ Shamir Secret Sharing scheme. + 2. The parties perform the secret-key operations (i.e., the protocols) in the same way as in the $N$-out-of-$N$-threshold scheme, yet among $t$ parties only and with $S(\alpha_i)\cdot l_i$ instead of $s_i$. + +However, the scheme has the downside of requiring to know set of parties participating to a given secret-key operation (i.e., evaluation of $F$). This is because evaluating $F(a, S(\alpha_i) \cdot l_i$ requires each party $i$ to compute the Lagrange coefficient $l_i$, which depends on the participating set. +Another downside of this scheme is that it requires a round of private, pairwise message exchanges between the parties before the scheme can be used in the $t$-out-of-$N$ regime. + +### Implementation + +Thanks to the $t$-out-of-$N$-threshold scheme being a direct extension of the $N$-out-of-$N$-threshold scheme (see the discussion above), the implementation of the former consist of two new types: `Thresholdizer` and `Combiner`. + +The `Thresholdizer` type implements the secret-key generation and re-sharing steps. This type corresponds to part 1. of the extension as described above. More specifically: +- `GenShamirPolynomial(threshold int, sk *rlwe.SecretKey)` generates the random degree-$(t-1)$ polynomial with `sk` as constant coefficient (i.e., $S_i$ above). +- `GenShamirSecretShare(recipient ShamirPublicPoint, [...])` generates re-sharing for a given recipient by evaluating the secret polynomial (i.e., $S_i(\alpha_j)$ above). +- `AggregateShares(share1, share2 ShamirSecretShare, [...])` aggregates two received shares (i.e., one addition step in computing $S(\alpha_i)$ above). + +The `Combiner` type lets parties obtain $t$-out-of-$t$ additive shares from their $t$-out-of-$N$ Shamir shares. This type corresponds to part 2. of the extension as described above, and is called as a pre-processing before any secret-key operation performed in the $t$-out-of-$N$ regime. More specifically, the `Combiner.GenAdditiveShare` takes as input the $t$-out-of-$N$-threshold secret-share of the party ($S(\alpha_i)$ above) along with the set $L=\{\alpha_1, ..., \alpha_t\}$ of the $t$ parties participating to the protocol, and computes: + +$$S(\alpha_i) \cdot l_i = S(\alpha_i) \cdot \prod_{\substack{\alpha_j \in L\\ \alpha_j \neq \alpha_i}} \frac{\alpha_j}{\alpha_j - \alpha_i}.$$ + +Hence, from the share output by `GenAdditiveShare`, the usual protocols described for the $N$-out-of-$N$-threshold setting (see the previous section) can be used, yet with $N=t$. ## MHE-MPC Protocol Overview From 95bef5b79e1cdbb244b3c4637517cf50b5d2d4a0 Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 11 Dec 2024 18:12:11 +0100 Subject: [PATCH 02/21] Adapted the int_pir example to the t-out-of-n-threshold scheme --- examples/multiparty/int_pir/main.go | 645 ++++++++++++++++++---------- 1 file changed, 412 insertions(+), 233 deletions(-) diff --git a/examples/multiparty/int_pir/main.go b/examples/multiparty/int_pir/main.go index 79074f00..736e660a 100644 --- a/examples/multiparty/int_pir/main.go +++ b/examples/multiparty/int_pir/main.go @@ -1,7 +1,41 @@ +// This example demonstrates the use of the [multiparty] package to perform a basic N-party private information retrival (PIR) protocol. +// This protocol relies on the t-out-of-N-threshold variant of the BGV scheme. +// +// In a nutshell, each party uploads an data row to a helper server, as an encrypted integer vector. The result is an encrypted database of N rows. At a later stage, +// a party queries a row in the database to the helper, without revealing the selector query index. The querier does so by encoding its query as +// a binary vector with a single 1 component corresponding to the row it wants to retrive, and send this encrypted vector to the helper. +// The helper can then compute the response (under encryption) by multiplying each row i of the database with the i-th query component, and summing +// the resulting vectors together. Finally, the result can be decrypted by any group of t parties. +// +// Thanks to the t-out-of-N-threshold scheme, only t parties need to be online at query time. +// +// For more details about the PIR circuit example see the paper [Multiparty Homomorphic Encryption from Ring-Learning-With-Errors] by Mouchet, Troncoso-Pastoriza, Bossuat, and Hubaux. +// For more details about the t-out-of-N-threshold scheme, see the paper [An Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic Encryption] by Mouchet, Bertrand and Hubaux. +// +// To run the example, use the following command: +// +// go run main.go N T NGoRoutines +// +// where N is the number of parties (default:3) T is the threshold (default: 2) and NGoRoutines is the number of Go routines (default: 1) to use during the homomorphic evaluation. +// All parties are run in the same process. +// +// The example demonstrates the following steps: +// +// 1. Setup: +// a. The parties generate threshold secret key with t-out-of-N access structure. +// b. The parties generate a collective public encryption key, a relinearization key, and a set of rotation (or, galois) keys. +// 2. Database inputs: Each party encrypts its input row vector and send it to a helper server. +// 3. Query evaluation: A party requests a row in the database. The helper server computes the query output as described above. +// 4. Query output decryption: the helper, with the help of at least t parties, decrypts the query output. +// +// [Multiparty Homomorphic Encryption from Ring-Learning-With-Errors]: https://eprint.iacr.org/2020/304 +// [An Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic Encryption]: https://eprint.iacr.org/2022/780 package main import ( + "errors" "log" + "math/rand" "os" "strconv" "sync" @@ -14,91 +48,78 @@ import ( "github.com/tuneinsight/lattigo/v6/utils/sampling" ) -func check(err error) { - if err != nil { - panic(err) - } -} - -func runTimed(f func()) time.Duration { - start := time.Now() - f() - return time.Since(start) -} - -func runTimedParty(f func(), N int) time.Duration { - start := time.Now() - f() - return time.Duration(time.Since(start).Nanoseconds() / int64(N)) -} - +// party is a type for the parties' state in the protocol, to be kept accross the different phases. type party struct { - sk *rlwe.SecretKey - rlkEphemSk *rlwe.SecretKey + multiparty.Combiner - ckgShare multiparty.PublicKeyGenShare - rkgShareOne multiparty.RelinearizationKeyGenShare - rkgShareTwo multiparty.RelinearizationKeyGenShare - gkgShare multiparty.GaloisKeyGenShare - cksShare multiparty.KeySwitchShare + sk *rlwe.SecretKey // secret key of the party + tsk multiparty.ShamirSecretShare + rlkEphemSk *rlwe.SecretKey // ephemeral state to be used in the RKG protocol - input []uint64 + shamirPt multiparty.ShamirPublicPoint + + input []uint64 // the input of the party, encoding a set as a binary vector } -type maskTask struct { - query *rlwe.Ciphertext - mask *rlwe.Plaintext - row *rlwe.Ciphertext - res *rlwe.Ciphertext - elapsedmaskTask time.Duration +// getOnlineParties is a utility function that returns t random parties from a list of parties. +// This simulates a dynamic setting where the system rely on any t parties to be online at query time to +// execute the various protocols. +func getOnlineParties(t int, parties []party) []party { + if t > len(parties) { + check(errors.New("t must be less than the number of parties")) + } + // randomizes a subset of t parties + onlineParties := make([]party, len(parties)) + copy(onlineParties, parties) + rand.Shuffle(len(onlineParties), func(i, j int) { onlineParties[i], onlineParties[j] = onlineParties[j], onlineParties[i] }) + return onlineParties[:t] } -var elapsedCKGCloud time.Duration -var elapsedCKGParty time.Duration -var elapsedRKGCloud time.Duration -var elapsedRKGParty time.Duration -var elapsedGKGCloud time.Duration -var elapsedGKGParty time.Duration -var elapsedCKSCloud time.Duration -var elapsedPCKSParty time.Duration -var elapsedRequestParty time.Duration -var elapsedRequestCloud time.Duration -var elapsedRequestCloudCPU time.Duration +// getShamirPoints is a utility function that returns the Shamir public points of a group of parties. +func getShamirPoints(parties []party) []multiparty.ShamirPublicPoint { + shamirPoints := make([]multiparty.ShamirPublicPoint, len(parties)) + for i := range parties { + shamirPoints[i] = parties[i].shamirPt + } + return shamirPoints +} + +var l = log.New(os.Stderr, "", 0) func main() { - // This example simulates a SMC instance of a private information retrieval (PIR) problem. - // The problem statement is as follows: a cloud stores data of several parties - // encrypted under a shared public-key. An external party wants to retrieve - // the plaintext content of one of the ciphertexts while ensuring the following - // security property: no information other than the fact that a request was made must - // be disclosed to the cloud, to the owners of the shared public-key or to anyone else. - // - // For more details see - // Multiparty Homomorphic Encryption: From Theory to Practice () - - l := log.New(os.Stderr, "", 0) - - // $go run main.go arg1 arg2 - // arg1: number of parties - // arg2: number of Go routines - // MinDelta number of parties for n=8192: 512 parties (this is a memory intensive process) + // Parse command line arguments N := 3 // Default number of parties var err error if len(os.Args[1:]) >= 1 { N, err = strconv.Atoi(os.Args[1]) check(err) + + if N < 3 || N > 128 { + l.Fatal("N must be in the range [3, 128]") + } } - NGoRoutine := 1 // Default number of Go routines + t := 2 // Default Threshold if len(os.Args[1:]) >= 2 { - NGoRoutine, err = strconv.Atoi(os.Args[2]) + t, err = strconv.Atoi(os.Args[2]) check(err) + + if t < 2 || t >= N { + l.Fatal("T must be in the range [2, N-1]") + } } - // Index of the ciphertext to retrieve. - queryIndex := 2 + nGoRoutine := 1 // Default number of Go routines + if len(os.Args[1:]) >= 3 { + nGoRoutine, err = strconv.Atoi(os.Args[2]) + check(err) + + if nGoRoutine < 1 { + l.Fatal("NGoRoutine must be at least 1") + } + } // Creating encryption parameters // LogN = 13 & LogQP = 218 @@ -112,57 +133,87 @@ func main() { panic(err) } - // Common reference polynomial generator that uses the PRNG + // The circuit relies on rotations/automorphisms for computing an inner-sum-like operation. + // This obtains the corresponding Galois elements. + galEls := append(params.GaloisElementsForInnerSum(1, params.N()>>1), params.GaloisElementForRowRotation()) + + // Creates a PRNG that will be used to sample the common reference string (crs) crs, err := sampling.NewKeyedPRNG([]byte{'l', 'a', 't', 't', 'i', 'g', 'o'}) if err != nil { panic(err) } - // Instantiation of each of the protocols needed for the PIR example + // Create the N input parties and generate their secret keys and private inputs + P := genparties(params, N, t) - // Create each party, and allocate the memory for all the shares that the protocols will need - P := genparties(params, N) + l.Println("> Setup phase") // TODO: improve the logging output - // 1) Collective public key generation - pk := ckgphase(params, crs, P) + // Step 1.a: Generation of the threshold secret key - // 2) Collective RelinearizationKey generation - RelinearizationKey := rkgphase(params, crs, P) + thresholdizer := multiparty.NewThresholdizer(params.Parameters) - // 3) Collective GaloisKeys generation - galKeys := gkgphase(params, crs, P) + // Allocates the memory for the parties' shares in the protocol + tskShares := make([][]multiparty.ShamirSecretShare, N) + for i := range P { + tskShares[i] = make([]multiparty.ShamirSecretShare, N) + for j := range P { + tskShares[i][j] = thresholdizer.AllocateThresholdSecretShare() + } + } - // Instantiates EvaluationKeySet - evk := rlwe.NewMemEvaluationKeySet(RelinearizationKey, galKeys...) + l.Println("> Threshold Secret Key Generation") + // The parties generate their shares for the threshold secret key + elapsedThresholdParty := runTimedParty(func() { + for i, pi := range P { + pol, err := thresholdizer.GenShamirPolynomial(t, pi.sk) + check(err) + for j, pj := range P { + thresholdizer.GenShamirSecretShare(pj.shamirPt, pol, &tskShares[i][j]) + } + } + }, N) + + // Each party aggregates the shares it has received from the other parties + elapsedThresholdParty += runTimedParty(func() { + for i := range P { + P[i].tsk = thresholdizer.AllocateThresholdSecretShare() + for j := range P { + err := thresholdizer.AggregateShares(tskShares[j][i], P[i].tsk, &P[i].tsk) // TODO wierd pointer arguments + check(err) + } + } + }, N) + + l.Printf("\tdone (cloud: %s, party: %s)\n", time.Duration(0), elapsedThresholdParty) + + // Step 1.b: Setup of the collective public encryption and evaluation keys + // Note 1: because we are using the t-out-of-N-threshold scheme, generating a key only requires t parties to be online. + // Note 2: the above note means that the workload could be balanced between the online parties whenever more than t parties are online. + + pk := execCKGProtocol(params, crs, getOnlineParties(t, P)) // Collective public key generation + + rlk := execRKGProtocol(params, crs, getOnlineParties(t, P)) // Collective RelinearizationKey generation + + galKeys := execGTGProtocol(params, crs, galEls, getOnlineParties(t, P)) // Collective GaloisKeys generation + + // Creates the evaluation key set from the rlk and the galKeys + evk := rlwe.NewMemEvaluationKeySet(rlk, galKeys...) l.Printf("\tSetup done (cloud: %s, party: %s)\n", elapsedCKGCloud+elapsedRKGCloud+elapsedGKGCloud, elapsedCKGParty+elapsedRKGParty+elapsedGKGParty) - // Pre-loading memory - encoder := bgv.NewEncoder(params) - l.Println("> Memory alloc Phase") - encInputs := make([]*rlwe.Ciphertext, N) - plainMask := make([]*rlwe.Plaintext, N) + // Step 2: Database inputs - // Ciphertexts to be retrieved + // Pre-allocates the encrypted database: one ciphertext per party + encInputs := make([]*rlwe.Ciphertext, N) for i := range encInputs { encInputs[i] = bgv.NewCiphertext(params, 1, params.MaxLevel()) } - // Plaintext masks: plainmask[i] = encode([0, ..., 0, 1_i, 0, ..., 0]) - // (zero with a 1 at the i-th position). - for i := range plainMask { - maskCoeffs := make([]uint64, params.N()) - maskCoeffs[i] = 1 - plainMask[i] = bgv.NewPlaintext(params, params.MaxLevel()) - if err := encoder.Encode(maskCoeffs, plainMask[i]); err != nil { - panic(err) - } - } - - // Ciphertexts encrypted under collective public key and stored in the cloud - l.Println("> Encrypt Phase") + // Each party encrypts its input row under the collective public key + l.Println("> Database input phase") + encoder := bgv.NewEncoder(params) encryptor := rlwe.NewEncryptor(params, pk) pt := bgv.NewPlaintext(params, params.MaxLevel()) elapsedEncryptParty := runTimedParty(func() { @@ -176,21 +227,34 @@ func main() { } }, N) - elapsedEncryptCloud := time.Duration(0) l.Printf("\tdone (cloud: %s, party: %s)\n", elapsedEncryptCloud, elapsedEncryptParty) - // Request phase - encQuery := genquery(params, queryIndex, encoder, encryptor) + // Step 3: Query evaluation - result := requestphase(params, queryIndex, NGoRoutine, encQuery, encInputs, plainMask, evk) + l.Println("> Query evaluation phase") - // Collective (partial) decryption (key switch) - encOut := cksphase(params, P, result) + queryIndex := 2 // Index of the ciphertext to retrieve. + querier := P[0] // Party performing the query + others := P[1:] // Other parties - l.Println("> ResulPlaintextModulus:") + // Creates a query ciphertext from the query index + encQuery := genQuery(params, queryIndex, encoder, encryptor) - // Decryption by the external party - decryptor := rlwe.NewDecryptor(params, P[0].sk) + // Executes the requests + encResult := execRequest(params, nGoRoutine, encQuery, encInputs, evk) + + // Step 4: Query output decryption + + // The helper, with the help of at least t parties, performs a re-encryption of the result towards the querier + participants := getOnlineParties(t-1, others) + encOut := execCKSProtocol(params, participants, querier, encResult) + + // The querier decrypts the final result + sk := rlwe.NewSecretKey(params) + err = querier.Combiner.GenAdditiveShare(append(getShamirPoints(participants), querier.shamirPt), querier.shamirPt, querier.tsk, sk) + check(err) + + decryptor := rlwe.NewDecryptor(params, sk) ptres := bgv.NewPlaintext(params, params.MaxLevel()) elapsedDecParty := runTimed(func() { decryptor.Decrypt(encOut, ptres) @@ -204,95 +268,73 @@ func main() { l.Printf("\t%v...%v\n", res[:8], res[params.N()-8:]) l.Printf("> Finished (total cloud: %s, total party: %s)\n", elapsedCKGCloud+elapsedRKGCloud+elapsedGKGCloud+elapsedEncryptCloud+elapsedRequestCloudCPU+elapsedCKSCloud, - elapsedCKGParty+elapsedRKGParty+elapsedGKGParty+elapsedEncryptParty+elapsedRequestParty+elapsedPCKSParty+elapsedDecParty) + elapsedCKGParty+elapsedRKGParty+elapsedGKGParty+elapsedEncryptParty+elapsedRequestParty+elapsedCKSParty+elapsedDecParty) } -func cksphase(params bgv.Parameters, P []*party, result *rlwe.Ciphertext) *rlwe.Ciphertext { - l := log.New(os.Stderr, "", 0) - - l.Println("> KeySwitch Phase") - - cks, err := multiparty.NewKeySwitchProtocol(params, ring.DiscreteGaussian{Sigma: 1 << 30, Bound: 6 * (1 << 30)}) // Collective public-key re-encryption - if err != nil { - panic(err) - } - - for _, pi := range P { - pi.cksShare = cks.AllocateShare(params.MaxLevel()) - } - - zero := rlwe.NewSecretKey(params) - cksCombined := cks.AllocateShare(params.MaxLevel()) - elapsedPCKSParty = runTimedParty(func() { - for _, pi := range P[1:] { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - cks.GenShare(pi.sk, zero, result, &pi.cksShare) - } - }, len(P)-1) - - encOut := bgv.NewCiphertext(params, 1, params.MaxLevel()) - elapsedCKSCloud = runTimed(func() { - for _, pi := range P { - if err := cks.AggregateShares(pi.cksShare, cksCombined, &cksCombined); err != nil { - panic(err) - } - } - cks.KeySwitch(result, cksCombined, encOut) - }) - l.Printf("\tdone (cloud: %s, party: %s)\n", elapsedCKSCloud, elapsedPCKSParty) - - return encOut -} - -func genparties(params bgv.Parameters, N int) []*party { - - P := make([]*party, N) +func genparties(params bgv.Parameters, N, t int) []party { + P := make([]party, N) kgen := rlwe.NewKeyGenerator(params) - for i := range P { - pi := &party{} - pi.sk = kgen.GenSecretKeyNew() + P[i].shamirPt = multiparty.ShamirPublicPoint(i + 1) - pi.input = make([]uint64, params.N()) - for j := range pi.input { - pi.input[j] = uint64(i) + P[i].sk = kgen.GenSecretKeyNew() + + P[i].input = make([]uint64, params.N()) + for j := range P[i].input { + P[i].input[j] = uint64(i) } + } - P[i] = pi + shamirPts := getShamirPoints(P) + for i := range P { + P[i].Combiner = multiparty.NewCombiner(params.Parameters, P[i].shamirPt, shamirPts, t) // TODO: NewCombiner takes interface } return P } -func ckgphase(params bgv.Parameters, crs sampling.PRNG, P []*party) *rlwe.PublicKey { - - l := log.New(os.Stderr, "", 0) +func execCKGProtocol(params bgv.Parameters, crs sampling.PRNG, participants []party) *rlwe.PublicKey { l.Println("> PublicKeyGen Phase") - ckg := multiparty.NewPublicKeyGenProtocol(params) // Public key generation + // Creates a protocol type for the collective public key generation. + // The type is stateless and can be used to generate as many public keys as needed. + ckg := multiparty.NewPublicKeyGenProtocol(params) - ckgCombined := ckg.AllocateShare() - for _, pi := range P { - pi.ckgShare = ckg.AllocateShare() + // Allocates the memory for the parties' shares in the protocol + ckgShares := make([]multiparty.PublicKeyGenShare, len(participants)) + tsks := make([]*rlwe.SecretKey, len(participants)) + for i := range ckgShares { + ckgShares[i] = ckg.AllocateShare() // the public CKG shares + tsks[i] = rlwe.NewSecretKey(params) // the t-out-of-t secret keys for group P } + ckgCombined := ckg.AllocateShare() // Allocate the memory for the combined share + // sample the common reference polynomial (crp) common reference string (crs) crp := ckg.SampleCRP(crs) + // Generate the parties' shares elapsedCKGParty = runTimedParty(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - ckg.GenShare(pi.sk, crp, &pi.ckgShare) - } - }, len(P)) + for i, pi := range participants { + // Generate the t-out-of-t secret key of the party within the group of participants + err := pi.Combiner.GenAdditiveShare(getShamirPoints(participants), pi.shamirPt, pi.tsk, tsks[i]) // TODO: discuss returning the key directly + check(err) + // Generate the public key share of the party from the t-out-of-t secret key + ckg.GenShare(tsks[i], crp, &ckgShares[i]) + } + }, len(participants)) + + // Aggregate the parties' shares into a collective public key pk := rlwe.NewPublicKey(params) - elapsedCKGCloud = runTimed(func() { - for _, pi := range P { - ckg.AggregateShares(pi.ckgShare, ckgCombined, &ckgCombined) + // Aggregate the parties' shares into a combined share + for i := range participants { + ckg.AggregateShares(ckgShares[i], ckgCombined, &ckgCombined) } + + // Generate the public key from the combined share ckg.GenPublicKey(ckgCombined, crp, pk) }) @@ -301,47 +343,65 @@ func ckgphase(params bgv.Parameters, crs sampling.PRNG, P []*party) *rlwe.Public return pk } -func rkgphase(params bgv.Parameters, crs sampling.PRNG, P []*party) *rlwe.RelinearizationKey { - l := log.New(os.Stderr, "", 0) +func execRKGProtocol(params bgv.Parameters, crs sampling.PRNG, participants []party) *rlwe.RelinearizationKey { l.Println("> RelinearizationKeyGen Phase") - rkg := multiparty.NewRelinearizationKeyGenProtocol(params) // Relineariation key generation + // Creates a protocol type for the collective relinearization key generation. + // The type is stateless and can be used to generate as many relinearization keys as needed. + // The RKG protocol has two rounds. Because the ephemeral secret key is not re-shared, + // the same set of participants must participate to the two rounds. + rkg := multiparty.NewRelinearizationKeyGenProtocol(params) + // Allocates the memory for the parties' shares in the protocol + rkgSharesRoundOne := make([]multiparty.RelinearizationKeyGenShare, len(participants)) + rkgSharesRoundTwo := make([]multiparty.RelinearizationKeyGenShare, len(participants)) + tsks := make([]*rlwe.SecretKey, len(participants)) + for i := range participants { + // the parties have a private ephemeral secret key in the RKGen protocol + participants[i].rlkEphemSk, rkgSharesRoundOne[i], rkgSharesRoundTwo[i] = rkg.AllocateShare() + tsks[i] = rlwe.NewSecretKey(params) + } + // Allocate the memory for the combined public shares _, rkgCombined1, rkgCombined2 := rkg.AllocateShare() - for _, pi := range P { - pi.rlkEphemSk, pi.rkgShareOne, pi.rkgShareTwo = rkg.AllocateShare() - } - + // Sample the common reference polynomial (crp) common reference string (crs) crp := rkg.SampleCRP(crs) + // The parties generate their shares for round one elapsedRKGParty = runTimedParty(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - rkg.GenShareRoundOne(pi.sk, crp, pi.rlkEphemSk, &pi.rkgShareOne) - } - }, len(P)) + for i, pi := range participants { + // Generate the t-out-of-t secret key of the party within the group of participants + err := pi.Combiner.GenAdditiveShare(getShamirPoints(participants), pi.shamirPt, pi.tsk, tsks[i]) + check(err) + + // Generate the shares for round one from the t-out-of-t secret key + rkg.GenShareRoundOne(tsks[i], crp, pi.rlkEphemSk, &rkgSharesRoundOne[i]) + } + }, len(participants)) + + // the helper aggregates the parties' shares for round one elapsedRKGCloud = runTimed(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - rkg.AggregateShares(pi.rkgShareOne, rkgCombined1, &rkgCombined1) + for i := range participants { + rkg.AggregateShares(rkgSharesRoundOne[i], rkgCombined1, &rkgCombined1) } }) + // The parties generate their shares for round two elapsedRKGParty += runTimedParty(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - rkg.GenShareRoundTwo(pi.rlkEphemSk, pi.sk, rkgCombined1, &pi.rkgShareTwo) + for i, pi := range participants { + // Generate the shares for round two from the t-out-of-t secret key + // Note: the same set of participants must participate to the two rounds, so the same tsk is used. + rkg.GenShareRoundTwo(pi.rlkEphemSk, tsks[i], rkgCombined1, &rkgSharesRoundTwo[i]) } - }, len(P)) + }, len(participants)) + // the helper aggregates the parties' shares for round two and generates the relinearization key rlk := rlwe.NewRelinearizationKey(params) elapsedRKGCloud += runTimed(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - rkg.AggregateShares(pi.rkgShareTwo, rkgCombined2, &rkgCombined2) + for i := range participants { + rkg.AggregateShares(rkgSharesRoundTwo[i], rkgCombined2, &rkgCombined2) } rkg.GenRelinearizationKey(rkgCombined1, rkgCombined2, rlk) }) @@ -351,54 +411,59 @@ func rkgphase(params bgv.Parameters, crs sampling.PRNG, P []*party) *rlwe.Reline return rlk } -func gkgphase(params bgv.Parameters, crs sampling.PRNG, P []*party) (galKeys []*rlwe.GaloisKey) { +func execGTGProtocol(params bgv.Parameters, crs sampling.PRNG, galEls []uint64, participants []party) (galKeys []*rlwe.GaloisKey) { - l := log.New(os.Stderr, "", 0) - - l.Println("> RTG Phase") + l.Println("> GKG Phase") + // Creates a protocol type for the collective galois key generation. + // The type is stateless and can be used to generate as many galois keys as needed. gkg := multiparty.NewGaloisKeyGenProtocol(params) // Rotation keys generation - for _, pi := range P { - pi.gkgShare = gkg.AllocateShare() + // Allocates the memory for the parties' shares in the protocol + gkgShares := make([]multiparty.GaloisKeyGenShare, len(participants)) + tsks := make([]*rlwe.SecretKey, len(participants)) + for i := range participants { + gkgShares[i] = gkg.AllocateShare() + tsks[i] = rlwe.NewSecretKey(params) } - galEls := append(params.GaloisElementsForInnerSum(1, params.N()>>1), params.GaloisElementForRowRotation()) + // Allocate a slice for storing the output keys galKeys = make([]*rlwe.GaloisKey, len(galEls)) - gkgShareCombined := gkg.AllocateShare() - - for i, galEl := range galEls { - - gkgShareCombined.GaloisElement = galEl + // Runs the GKG protocol for each required Galois key + // Note: this demo re-uses the allocated shares for each execution. + for j, galEl := range galEls { + // Sample the common reference polynomial (crp) common reference string (crs) crp := gkg.SampleCRP(crs) + // The parties generate their shares for the Galois key generation protocol elapsedGKGParty += runTimedParty(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - if err := gkg.GenShare(pi.sk, galEl, crp, &pi.gkgShare); err != nil { - panic(err) - } + for i, pi := range participants { + // Generate the t-out-of-t secret key of the party within the group of participants + err := pi.Combiner.GenAdditiveShare(getShamirPoints(participants), pi.shamirPt, pi.tsk, tsks[i]) + check(err) + + // Generate the shares for the Galois key generation protocol from the t-out-of-t secret key + err = gkg.GenShare(tsks[i], galEl, crp, &gkgShares[i]) + check(err) } - }, len(P)) + }, len(participants)) + // The helper aggregates the parties' shares and generates the Galois key elapsedGKGCloud += runTimed(func() { - if err := gkg.AggregateShares(P[0].gkgShare, P[1].gkgShare, &gkgShareCombined); err != nil { - panic(err) + gkgShareCombined := gkg.AllocateShare() // Allocate the memory for the combined share + gkgShareCombined.GaloisElement = galEl + for i := range participants { + err := gkg.AggregateShares(gkgShares[i], gkgShareCombined, &gkgShareCombined) + check(err) } - for _, pi := range P[2:] { - if err := gkg.AggregateShares(pi.gkgShare, gkgShareCombined, &gkgShareCombined); err != nil { - panic(err) - } - } + galKeys[j] = rlwe.NewGaloisKey(params) - galKeys[i] = rlwe.NewGaloisKey(params) - - if err := gkg.GenGaloisKey(gkgShareCombined, crp, galKeys[i]); err != nil { + if err := gkg.GenGaloisKey(gkgShareCombined, crp, galKeys[j]); err != nil { panic(err) } }) @@ -408,40 +473,67 @@ func gkgphase(params bgv.Parameters, crs sampling.PRNG, P []*party) (galKeys []* return } -func genquery(params bgv.Parameters, queryIndex int, encoder *bgv.Encoder, encryptor *rlwe.Encryptor) *rlwe.Ciphertext { - // Query ciphertext +func genQuery(params bgv.Parameters, queryIndex int, encoder *bgv.Encoder, encryptor *rlwe.Encryptor) *rlwe.Ciphertext { + + l.Println("> QueryGen Phase") + + // Creates a query vector from the query index queryCoeffs := make([]uint64, params.N()) queryCoeffs[queryIndex] = 1 + + // Encrypts the query vector query := bgv.NewPlaintext(params, params.MaxLevel()) var encQuery *rlwe.Ciphertext elapsedRequestParty += runTimed(func() { - var err error - if err = encoder.Encode(queryCoeffs, query); err != nil { - panic(err) - } - if encQuery, err = encryptor.EncryptNew(query); err != nil { - panic(err) - } + + err := encoder.Encode(queryCoeffs, query) + check(err) + + encQuery, err = encryptor.EncryptNew(query) + check(err) }) return encQuery } -func requestphase(params bgv.Parameters, queryIndex, NGoRoutine int, encQuery *rlwe.Ciphertext, encInputs []*rlwe.Ciphertext, plainMask []*rlwe.Plaintext, evk rlwe.EvaluationKeySet) *rlwe.Ciphertext { +func execRequest(params bgv.Parameters, NGoRoutine int, encQuery *rlwe.Ciphertext, encInputs []*rlwe.Ciphertext, evk rlwe.EvaluationKeySet) *rlwe.Ciphertext { - l := log.New(os.Stderr, "", 0) + // First, pre-compute the some plaintext masks for the query evaluation as: + // plainmask[i] = encode([0, ..., 0, 1, 0, ..., 0]) (zero with a 1 at the i-th position). + // In practice, the masks are pre-computed and reused accross queries. + encoder := bgv.NewEncoder(params) + plainMask := make([]*rlwe.Plaintext, len(encInputs)) + for i := range plainMask { + maskCoeffs := make([]uint64, params.N()) + maskCoeffs[i] = 1 + plainMask[i] = bgv.NewPlaintext(params, params.MaxLevel()) + if err := encoder.Encode(maskCoeffs, plainMask[i]); err != nil { + panic(err) + } + } l.Println("> Request Phase") - // Buffer for the intermediate computation done by the cloud + // Buffer for the intermediate computation done by the helper encPartial := make([]*rlwe.Ciphertext, len(encInputs)) for i := range encPartial { encPartial[i] = bgv.NewCiphertext(params, 2, params.MaxLevel()) } + // Creates an evaluator for the homomorphic evaluation evaluator := bgv.NewEvaluator(params, evk) // Split the task among the Go routines + + // maskTask is a type for the task to be executed by the Go routines + // The task computes the multiplication of the query with a mask, and the multiplication of the result with a row of the database. + type maskTask struct { + query *rlwe.Ciphertext + mask *rlwe.Plaintext + row *rlwe.Ciphertext + res *rlwe.Ciphertext + elapsedmaskTask time.Duration + } tasks := make(chan *maskTask) workers := &sync.WaitGroup{} workers.Add(NGoRoutine) @@ -475,10 +567,8 @@ func requestphase(params bgv.Parameters, queryIndex, NGoRoutine int, encQuery *r } }) } - //l.Println("\t evaluator", i, "down") workers.Done() }(i) - //l.Println("\t evaluator", i, "started") } taskList := make([]*maskTask, 0) @@ -495,17 +585,21 @@ func requestphase(params bgv.Parameters, queryIndex, NGoRoutine int, encQuery *r tasks <- task } close(tasks) - workers.Wait() + workers.Wait() // Wait for all the workers to finish }) + // collects the elapsed time for each task for _, t := range taskList { elapsedRequestCloudCPU += t.elapsedmaskTask } - resultDeg2 := bgv.NewCiphertext(params, 2, params.MaxLevel()) - result := bgv.NewCiphertext(params, 1, params.MaxLevel()) + // Creates ciphertexts to store the final result + resultDeg2 := bgv.NewCiphertext(params, 2, params.MaxLevel()) // to receive the sum of the partial results. + result := bgv.NewCiphertext(params, 1, params.MaxLevel()) // to receive the relinearized final result // Summation of all the partial result among the different Go routines + // The sum is computed over the degree-2 ciphertexts from the previous step. Then, the result is relinearized. + // This avoids performing N relinearizations. finalAddDuration := runTimed(func() { for i := 0; i < len(encInputs); i++ { if err := evaluator.Add(resultDeg2, encPartial[i], resultDeg2); err != nil { @@ -525,3 +619,88 @@ func requestphase(params bgv.Parameters, queryIndex, NGoRoutine int, encQuery *r return result } + +func execCKSProtocol(params bgv.Parameters, participants []party, receiver party, ctIn *rlwe.Ciphertext) *rlwe.Ciphertext { + + l.Println("> KeySwitch Phase") + + // Creates a protocol type for the collective key-switching protocol, with smudging distribution parameter of 2^30. + // The type is stateless and can be used to generate as many key-switching keys as needed. + cks, err := multiparty.NewKeySwitchProtocol(params, ring.DiscreteGaussian{Sigma: 1 << 30, Bound: 6 * (1 << 30)}) + check(err) + + // Allocates the memory for the parties' shares in the protocol + cksShares := make([]multiparty.KeySwitchShare, len(participants)) + tsks := make([]*rlwe.SecretKey, len(participants)) + for i := range participants { + cksShares[i] = cks.AllocateShare(params.MaxLevel()) // Allocate the memory for the public share + tsks[i] = rlwe.NewSecretKey(params) // Allocate the memory for the t-out-of-t secret key + } + cksCombined := cks.AllocateShare(params.MaxLevel()) // Allocate the memory for the combined share + + // To generate a re-encryption of the result ciphertexts towards the querier, + // each party except for the receiver generates a key-switching share towards + // secret-key zero (i.e., a decryption share). + zero := rlwe.NewSecretKey(params) + + // The parties (except the receiver) generate their shares for the key-switching protocol + elapsedCKSParty = runTimedParty(func() { + for i, pi := range participants { + + // Generate the t-out-of-t secret key with the reciever and t-1 other parties + err := pi.Combiner.GenAdditiveShare(append(getShamirPoints(participants), receiver.shamirPt), pi.shamirPt, pi.tsk, tsks[i]) + check(err) + + // Generate the key-switching share with the t-out-of-t secret key + cks.GenShare(tsks[i], zero, ctIn, &cksShares[i]) + } + }, len(participants)) + + // The helper aggregates the parties' shares and generates the key-switching key + ctOut := bgv.NewCiphertext(params, 1, params.MaxLevel()) + elapsedCKSCloud = runTimed(func() { + // Aggregate the parties' shares into a combined share + for i := range participants { + err := cks.AggregateShares(cksShares[i], cksCombined, &cksCombined) + check(err) + } + // Generate the re-encryption from the combined share + cks.KeySwitch(ctIn, cksCombined, ctOut) + }) + l.Printf("\tdone (cloud: %s, party: %s)\n", elapsedCKSCloud, elapsedCKSParty) + + return ctOut +} + +var ( + elapsedCKGCloud time.Duration + elapsedCKGParty time.Duration + elapsedRKGCloud time.Duration + elapsedRKGParty time.Duration + elapsedGKGCloud time.Duration + elapsedGKGParty time.Duration + elapsedCKSCloud time.Duration + elapsedCKSParty time.Duration + elapsedEncryptCloud time.Duration + elapsedRequestParty time.Duration + elapsedRequestCloud time.Duration + elapsedRequestCloudCPU time.Duration +) + +func check(err error) { + if err != nil { + l.Fatal(err) + } +} + +func runTimed(f func()) time.Duration { + start := time.Now() + f() + return time.Since(start) +} + +func runTimedParty(f func(), N int) time.Duration { + start := time.Now() + f() + return time.Duration(time.Since(start).Nanoseconds() / int64(N)) +} From 97040742422eff6893db18bb26d56c996eb9b4d2 Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 11 Dec 2024 18:14:34 +0100 Subject: [PATCH 03/21] Improved the int_psi example readability and documentation --- examples/multiparty/int_psi/main.go | 494 +++++++++++++++------------- 1 file changed, 265 insertions(+), 229 deletions(-) diff --git a/examples/multiparty/int_psi/main.go b/examples/multiparty/int_psi/main.go index d1dee4a4..e7b51714 100644 --- a/examples/multiparty/int_psi/main.go +++ b/examples/multiparty/int_psi/main.go @@ -1,3 +1,26 @@ +// This example demonstrates the use of the [multiparty] package to perform a basic N-party private set intersection (PSI) protocol with external receiver. +// This protocol relies on the N-out-of-N threshold variant of the BGV scheme. +// In a nutshell, the parties encode their sets as binary vectors, and a helper server compute (under encryption) the intersection as the compoenent-wise product of all vectors. +// The result is then re-encrypted towards the receiver's key. +// For more details about the PSI circuit example see the paper [Multiparty Homomorphic Encryption from Ring-Learning-With-Errors] by by Christian Mouchet, Juan Troncoso-Pastoriza, Jean-Philippe Bossuat, and Jean-Pierre Hubaux. +// +// To run the example, use the following command: +// +// go run main.go N NGoRoutines +// +// where N is the number of parties (default:16) and NGoRoutines is the number of Go routines (default: 1) to use during the homomorphic evaluation. +// All parties are run in the same process. +// +// The example demonstrates the following steps: +// +// 1. Setup: The parties generate a collectice public encryption key and a collective relinearization key. +// 2. Inputs: Each party encrypts its input vector and send it to a helper server. +// 3. Evaluation: The helper server computes the multiplication of the input vectors and relinearizes the result. +// 4. Output: The helper server, with the help of the N parties, switches the encryption of the result to the target public key. +// 5. Decryption: The target party decrypts the result with its secret key. +// +// [Multiparty Homomorphic Encryption from Ring-Learning-With-Errors]: https://eprint.iacr.org/2020/304 +// TODO: do we want a README version of this docstring ? package main import ( @@ -14,77 +37,40 @@ import ( "github.com/tuneinsight/lattigo/v6/utils/sampling" ) -func check(err error) { - if err != nil { - panic(err) - } -} - -func runTimed(f func()) time.Duration { - start := time.Now() - f() - return time.Since(start) -} - -func runTimedParty(f func(), N int) time.Duration { - start := time.Now() - f() - return time.Duration(time.Since(start).Nanoseconds() / int64(N)) -} - +// party is a type for the parties' state in the protocol, to be kept accross the different phases. type party struct { - sk *rlwe.SecretKey - rlkEphemSk *rlwe.SecretKey + sk *rlwe.SecretKey // secret key of the party + rlkEphemSk *rlwe.SecretKey // ephemeral state to be used in the RKG protocol - ckgShare multiparty.PublicKeyGenShare - rkgShareOne multiparty.RelinearizationKeyGenShare - rkgShareTwo multiparty.RelinearizationKeyGenShare - pcksShare multiparty.PublicKeySwitchShare - - input []uint64 -} -type multTask struct { - wg *sync.WaitGroup - op1 *rlwe.Ciphertext - opOut *rlwe.Ciphertext - res *rlwe.Ciphertext - elapsedmultTask time.Duration + input []uint64 // the input of the party, encoding a set as a binary vector } -var elapsedEncryptParty time.Duration -var elapsedEncryptCloud time.Duration -var elapsedCKGCloud time.Duration -var elapsedCKGParty time.Duration -var elapsedRKGCloud time.Duration -var elapsedRKGParty time.Duration -var elapsedPCKSCloud time.Duration -var elapsedPCKSParty time.Duration -var elapsedEvalCloudCPU time.Duration -var elapsedEvalCloud time.Duration -var elapsedEvalParty time.Duration +var l = log.New(os.Stderr, "", 0) func main() { - // For more details about the PSI example see - // Multiparty Homomorphic Encryption: From Theory to Practice () - l := log.New(os.Stderr, "", 0) - - // $go run main.go arg1 arg2 - // arg1: number of parties - // arg2: number of Go routines - - // Largest for n=8192: 512 parties + // $go run main.go N NGoRoutine + // N: number of parties + // NGoRoutines: number of Go routines N := 16 // Default number of parties var err error if len(os.Args[1:]) >= 1 { N, err = strconv.Atoi(os.Args[1]) check(err) + + if N < 2 || N > 128 { + l.Fatal("N must be in the range [2, ..., 128]") + } } NGoRoutine := 1 // Default number of Go routines if len(os.Args[1:]) >= 2 { NGoRoutine, err = strconv.Atoi(os.Args[2]) check(err) + + if NGoRoutine < 1 { + l.Fatal("NGoRoutine must be at least 1") + } } // Creating encryption parameters from a default params with logN=14, logQP=438 with a plaintext modulus T=65537 @@ -94,46 +80,44 @@ func main() { LogP: []int{55, 55}, PlaintextModulus: 65537, }) - if err != nil { - panic(err) - } + check(err) + // Creates a PRNG that will be used to sample the common reference string (crs) crs, err := sampling.NewKeyedPRNG([]byte{'l', 'a', 't', 't', 'i', 'g', 'o'}) - if err != nil { - panic(err) - } + check(err) - encoder := bgv.NewEncoder(params) - - // Target private and public keys + // Generate some keys for the receiver (target party) tsk, tpk := rlwe.NewKeyGenerator(params).GenKeyPairNew() - // Create each party, and allocate the memory for all the shares that the protocols will need + // Create the N input parties and generate their secret keys P := genparties(params, N) - // Inputs & expected result - expRes := genInputs(params, P) + // Step 1: Setup of the collective public key and relinearization key - // 1) Collective public key generation - pk := ckgphase(params, crs, P) + pk := execCKGProtocol(params, crs, P) // generates the collective public key - // 2) Collective relinearization key generation - rlk := rkgphase(params, crs, P) + rlk := execRKGProtocol(params, crs, P) // generates the collective relinearization key - evk := rlwe.NewMemEvaluationKeySet(rlk) + evk := rlwe.NewMemEvaluationKeySet(rlk) // creates the evaluation key from the relinearization key l.Printf("\tdone (cloud: %s, party: %s)\n", elapsedRKGCloud, elapsedRKGParty) l.Printf("\tSetup done (cloud: %s, party: %s)\n", elapsedRKGCloud+elapsedCKGCloud, elapsedRKGParty+elapsedCKGParty) - encInputs := encPhase(params, P, pk, encoder) + // Step 2: Each party encrypts its input vector + expRes := genInputs(params, P) // generates the input vectors and the expected result + encoder := bgv.NewEncoder(params) + encInputs := inputPhase(params, P, pk, encoder) // encrypts the input vectors + + // Step 3: The helper server computes the multiplication of the input vectors and relinearizes the result encRes := evalPhase(params, NGoRoutine, encInputs, evk) - encOut := pcksPhase(params, tpk, encRes, P) + // Step 4: The helper server switches the encryption of the result to the target public key + encOut := execPCKSProtocol(params, tpk, encRes, P) - // Decrypt the result with the target secret key + // Step 5: The target party decrypts the result with its secret key l.Println("> ResulPlaintextModulus:") decryptor := rlwe.NewDecryptor(params, tsk) ptres := bgv.NewPlaintext(params, params.MaxLevel()) @@ -143,9 +127,9 @@ func main() { // Check the result res := make([]uint64, params.MaxSlots()) - if err := encoder.Decode(ptres, res); err != nil { - panic(err) - } + err = encoder.Decode(ptres, res) + check(err) + l.Printf("\t%v\n", res[:16]) for i := range expRes { if expRes[i] != res[i] { @@ -158,13 +142,143 @@ func main() { l.Printf("> Finished (total cloud: %s, total party: %s)\n", elapsedCKGCloud+elapsedRKGCloud+elapsedEncryptCloud+elapsedEvalCloud+elapsedPCKSCloud, elapsedCKGParty+elapsedRKGParty+elapsedEncryptParty+elapsedEvalParty+elapsedPCKSParty+elapsedDecParty) - } -func encPhase(params bgv.Parameters, P []*party, pk *rlwe.PublicKey, encoder *bgv.Encoder) (encInputs []*rlwe.Ciphertext) { +func genparties(params bgv.Parameters, N int) []party { - l := log.New(os.Stderr, "", 0) + // Create the parties and generates a secret key for each party + P := make([]party, N) + for i := range P { + P[i].sk = rlwe.NewKeyGenerator(params).GenSecretKeyNew() + } + return P +} + +func execCKGProtocol(params bgv.Parameters, crs sampling.PRNG, P []party) *rlwe.PublicKey { + + l.Println("> PublicKeyGen Phase") + + // Creates a protocol type for the collective public key generation. + // The type is stateless and can be used to generate as many public keys as needed. + ckg := multiparty.NewPublicKeyGenProtocol(params) + + // Allocates the memory for the parties' shares in the protocol + ckgShares := make([]multiparty.PublicKeyGenShare, len(P)) + for i := range ckgShares { + ckgShares[i] = ckg.AllocateShare() + } + ckgCombined := ckg.AllocateShare() // Allocate the memory for the combined share + + // sample the common reference polynomial (crp) common reference string (crs) + crp := ckg.SampleCRP(crs) + + // Generate the parties' shares + elapsedCKGParty = runTimedParty(func() { + for i, pi := range P { + ckg.GenShare(pi.sk, crp, &ckgShares[i]) + } + }, len(P)) + + // Aggregate the parties' shares into a collective public key + pk := rlwe.NewPublicKey(params) + elapsedCKGCloud = runTimed(func() { + // Aggregate the parties' shares into a combined share + for i := range P { + ckg.AggregateShares(ckgShares[i], ckgCombined, &ckgCombined) + } + + // Generate the public key from the combined share + ckg.GenPublicKey(ckgCombined, crp, pk) + }) + + l.Printf("\tdone (cloud: %s, party: %s)\n", elapsedCKGCloud, elapsedCKGParty) + + return pk +} + +func execRKGProtocol(params bgv.Parameters, crs sampling.PRNG, P []party) *rlwe.RelinearizationKey { + + l.Println("> RelinearizationKeyGen Phase") + + // Creates a protocol type for the collective relinearization key generation. + // The type is stateless and can be used to generate as many relinearization keys as needed. + // The RKG protocol has two rounds. + rkg := multiparty.NewRelinearizationKeyGenProtocol(params) + + // Allocates the memory for the parties' shares in the protocol + rkgSharesRoundOne := make([]multiparty.RelinearizationKeyGenShare, len(P)) + rkgSharesRoundTwo := make([]multiparty.RelinearizationKeyGenShare, len(P)) + for i := range P { + P[i].rlkEphemSk, rkgSharesRoundOne[i], rkgSharesRoundTwo[i] = rkg.AllocateShare() + // the parties have a private ephemeral secret key in the RKGen protocol + } + // Allocate the memory for the combined public shares + _, rkgCombined1, rkgCombined2 := rkg.AllocateShare() + + // Sample the common reference polynomial (crp) common reference string (crs) + crp := rkg.SampleCRP(crs) + + // The parties generate their shares for round one + elapsedRKGParty = runTimedParty(func() { + for i, pi := range P { + rkg.GenShareRoundOne(pi.sk, crp, pi.rlkEphemSk, &rkgSharesRoundOne[i]) + } + }, len(P)) + + // the helper aggregates the parties' shares for round one + elapsedRKGCloud = runTimed(func() { + for i := range P { + rkg.AggregateShares(rkgSharesRoundOne[i], rkgCombined1, &rkgCombined1) + } + }) + + // The parties generate their shares for round two + elapsedRKGParty += runTimedParty(func() { + for i, pi := range P { + rkg.GenShareRoundTwo(pi.rlkEphemSk, pi.sk, rkgCombined1, &rkgSharesRoundTwo[i]) + } + }, len(P)) + + // the helper aggregates the parties' shares for round two and generates the relinearization key + rlk := rlwe.NewRelinearizationKey(params) + elapsedRKGCloud += runTimed(func() { + for i := range P { + rkg.AggregateShares(rkgSharesRoundTwo[i], rkgCombined2, &rkgCombined2) + } + rkg.GenRelinearizationKey(rkgCombined1, rkgCombined2, rlk) + }) + + l.Printf("\tdone (cloud: %s, party: %s)\n", elapsedRKGCloud, elapsedRKGParty) + + return rlk +} + +func genInputs(params bgv.Parameters, P []party) (expRes []uint64) { + + // generate input vectors for the parties of max size + expRes = make([]uint64, params.MaxSlots()) + for i := range expRes { + expRes[i] = 1 + } + + for i := range P { + P[i].input = make([]uint64, params.MaxSlots()) + for j := range P[i].input { + if sampling.RandFloat64(0, 1) > 0.3 || j == 4 { + P[i].input[j] = 1 + } + expRes[j] *= P[i].input[j] + } + + } + + return +} + +func inputPhase(params bgv.Parameters, P []party, pk *rlwe.PublicKey, encoder *bgv.Encoder) (encInputs []*rlwe.Ciphertext) { + + // Allocate the memory for the encrypted input vectors encInputs = make([]*rlwe.Ciphertext, len(P)) for i := range encInputs { encInputs[i] = bgv.NewCiphertext(params, 1, params.MaxLevel()) @@ -177,12 +291,10 @@ func encPhase(params bgv.Parameters, P []*party, pk *rlwe.PublicKey, encoder *bg pt := bgv.NewPlaintext(params, params.MaxLevel()) elapsedEncryptParty = runTimedParty(func() { for i, pi := range P { - if err := encoder.Encode(pi.input, pt); err != nil { - panic(err) - } - if err := encryptor.Encrypt(pt, encInputs[i]); err != nil { - panic(err) - } + err := encoder.Encode(pi.input, pt) + check(err) + err = encryptor.Encrypt(pt, encInputs[i]) + check(err) } }, len(P)) @@ -194,8 +306,10 @@ func encPhase(params bgv.Parameters, P []*party, pk *rlwe.PublicKey, encoder *bg func evalPhase(params bgv.Parameters, NGoRoutine int, encInputs []*rlwe.Ciphertext, evk rlwe.EvaluationKeySet) (encRes *rlwe.Ciphertext) { - l := log.New(os.Stderr, "", 0) + // The eval phase performs the multiplication as a balanced binary tree. + // For each level of the tree, it performs the multiplications in parallel using at most NGoRoutine Go routines. + // Allocate the memory for the encrypted result at each level of the tree encLvls := make([][]*rlwe.Ciphertext, 0) encLvls = append(encLvls, encInputs) for nLvl := len(encInputs) / 2; nLvl > 0; nLvl = nLvl >> 1 { @@ -207,8 +321,18 @@ func evalPhase(params bgv.Parameters, NGoRoutine int, encInputs []*rlwe.Cipherte } encRes = encLvls[len(encLvls)-1][0] + // Creates a evaluator for the multiplication, with the evaluation key evaluator := bgv.NewEvaluator(params, evk, true) + // Split the task among the Go routines + // A multTask is a task that multiplies two ciphertexts and relinearizes the result + type multTask struct { + wg *sync.WaitGroup + op1 *rlwe.Ciphertext + opOut *rlwe.Ciphertext + res *rlwe.Ciphertext + elapsedmultTask time.Duration + } tasks := make(chan *multTask) workers := &sync.WaitGroup{} workers.Add(NGoRoutine) @@ -219,20 +343,17 @@ func evalPhase(params bgv.Parameters, NGoRoutine int, encInputs []*rlwe.Cipherte for task := range tasks { task.elapsedmultTask = runTimed(func() { // 1) Multiplication of two input vectors - if err := evaluator.Mul(task.op1, task.opOut, task.res); err != nil { - panic(err) - } + err := evaluator.Mul(task.op1, task.opOut, task.res) + check(err) // 2) Relinearization - if err := evaluator.Relinearize(task.res, task.res); err != nil { - panic(err) - } + err = evaluator.Relinearize(task.res, task.res) + check(err) + }) task.wg.Done() } - //l.Println("\t evaluator", i, "down") workers.Done() }(i) - //l.Println("\t evaluator", i, "started") } // Start the tasks @@ -267,75 +388,42 @@ func evalPhase(params bgv.Parameters, NGoRoutine int, encInputs []*rlwe.Cipherte return } -func genparties(params bgv.Parameters, N int) []*party { - - // Create each party, and allocate the memory for all the shares that the protocols will need - P := make([]*party, N) - for i := range P { - pi := &party{} - pi.sk = rlwe.NewKeyGenerator(params).GenSecretKeyNew() - - P[i] = pi - } - - return P -} - -func genInputs(params bgv.Parameters, P []*party) (expRes []uint64) { - - expRes = make([]uint64, params.MaxSlots()) - for i := range expRes { - expRes[i] = 1 - } - - for _, pi := range P { - - pi.input = make([]uint64, params.MaxSlots()) - for i := range pi.input { - if sampling.RandFloat64(0, 1) > 0.3 || i == 4 { - pi.input[i] = 1 - } - expRes[i] *= pi.input[i] - } - - } - - return -} - -func pcksPhase(params bgv.Parameters, tpk *rlwe.PublicKey, encRes *rlwe.Ciphertext, P []*party) (encOut *rlwe.Ciphertext) { - - l := log.New(os.Stderr, "", 0) +func execPCKSProtocol(params bgv.Parameters, tpk *rlwe.PublicKey, encRes *rlwe.Ciphertext, P []party) (encOut *rlwe.Ciphertext) { // Collective key switching from the collective secret key to // the target public key - - pcks, err := multiparty.NewPublicKeySwitchProtocol(params, ring.DiscreteGaussian{Sigma: 1 << 30, Bound: 6 * (1 << 30)}) - if err != nil { - panic(err) - } - - for _, pi := range P { - pi.pcksShare = pcks.AllocateShare(params.MaxLevel()) - } - l.Println("> PublicKeySwitch Phase") + + // Creates a protocol type for the collective public key switch. + // The type is stateless and can be used to generate as many public key switches as needed. + pcks, err := multiparty.NewPublicKeySwitchProtocol(params, ring.DiscreteGaussian{Sigma: 1 << 30, Bound: 6 * (1 << 30)}) + check(err) + + // Allocates the memory for the parties' shares in the protocol + pcksShares := make([]multiparty.PublicKeySwitchShare, len(P)) + for i := range P { + pcksShares[i] = pcks.AllocateShare(params.MaxLevel()) + } + // Allocates the memory for combined share + pcksCombined := pcks.AllocateShare(params.MaxLevel()) + + // Each party generates its share elapsedPCKSParty = runTimedParty(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - pcks.GenShare(pi.sk, tpk, encRes, &pi.pcksShare) + for i, pi := range P { + pcks.GenShare(pi.sk, tpk, encRes, &pcksShares[i]) } }, len(P)) - pcksCombined := pcks.AllocateShare(params.MaxLevel()) + // The helper server aggregates the parties' shares and combutes the output, re-encrpyted, ciphertext encOut = bgv.NewCiphertext(params, 1, params.MaxLevel()) elapsedPCKSCloud = runTimed(func() { - for _, pi := range P { - if err = pcks.AggregateShares(pi.pcksShare, pcksCombined, &pcksCombined); err != nil { - panic(err) - } + // Aggregate the parties' shares into a combined share + for i := range P { + err := pcks.AggregateShares(pcksShares[i], pcksCombined, &pcksCombined) + check(err) } + // Generate the output ciphertext pcks.KeySwitch(encRes, pcksCombined, encOut) }) l.Printf("\tdone (cloud: %s, party: %s)\n", elapsedPCKSCloud, elapsedPCKSParty) @@ -343,86 +431,34 @@ func pcksPhase(params bgv.Parameters, tpk *rlwe.PublicKey, encRes *rlwe.Cipherte return } -func rkgphase(params bgv.Parameters, crs sampling.PRNG, P []*party) *rlwe.RelinearizationKey { - l := log.New(os.Stderr, "", 0) +var ( + elapsedEncryptParty, + elapsedEncryptCloud, + elapsedCKGCloud, + elapsedCKGParty, + elapsedRKGCloud, + elapsedRKGParty, + elapsedPCKSCloud, + elapsedPCKSParty, + elapsedEvalCloudCPU, + elapsedEvalCloud, + elapsedEvalParty time.Duration +) - l.Println("> RelinearizationKeyGen Phase") - - rkg := multiparty.NewRelinearizationKeyGenProtocol(params) // Relineariation key generation - _, rkgCombined1, rkgCombined2 := rkg.AllocateShare() - - for _, pi := range P { - pi.rlkEphemSk, pi.rkgShareOne, pi.rkgShareTwo = rkg.AllocateShare() +func check(err error) { + if err != nil { + l.Fatal(err) } - - crp := rkg.SampleCRP(crs) - - elapsedRKGParty = runTimedParty(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - rkg.GenShareRoundOne(pi.sk, crp, pi.rlkEphemSk, &pi.rkgShareOne) - } - }, len(P)) - - elapsedRKGCloud = runTimed(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - rkg.AggregateShares(pi.rkgShareOne, rkgCombined1, &rkgCombined1) - } - }) - - elapsedRKGParty += runTimedParty(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - rkg.GenShareRoundTwo(pi.rlkEphemSk, pi.sk, rkgCombined1, &pi.rkgShareTwo) - } - }, len(P)) - - rlk := rlwe.NewRelinearizationKey(params) - elapsedRKGCloud += runTimed(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - rkg.AggregateShares(pi.rkgShareTwo, rkgCombined2, &rkgCombined2) - } - rkg.GenRelinearizationKey(rkgCombined1, rkgCombined2, rlk) - }) - - l.Printf("\tdone (cloud: %s, party: %s)\n", elapsedRKGCloud, elapsedRKGParty) - - return rlk } -func ckgphase(params bgv.Parameters, crs sampling.PRNG, P []*party) *rlwe.PublicKey { - - l := log.New(os.Stderr, "", 0) - - l.Println("> PublicKeyGen Phase") - - ckg := multiparty.NewPublicKeyGenProtocol(params) // Public key generation - ckgCombined := ckg.AllocateShare() - for _, pi := range P { - pi.ckgShare = ckg.AllocateShare() - } - - crp := ckg.SampleCRP(crs) - - elapsedCKGParty = runTimedParty(func() { - for _, pi := range P { - /* #nosec G601 -- Implicit memory aliasing in for loop acknowledged */ - ckg.GenShare(pi.sk, crp, &pi.ckgShare) - } - }, len(P)) - - pk := rlwe.NewPublicKey(params) - - elapsedCKGCloud = runTimed(func() { - for _, pi := range P { - ckg.AggregateShares(pi.ckgShare, ckgCombined, &ckgCombined) - } - ckg.GenPublicKey(ckgCombined, crp, pk) - }) - - l.Printf("\tdone (cloud: %s, party: %s)\n", elapsedCKGCloud, elapsedCKGParty) - - return pk +func runTimed(f func()) time.Duration { + start := time.Now() + f() + return time.Since(start) +} + +func runTimedParty(f func(), N int) time.Duration { + start := time.Now() + f() + return time.Duration(time.Since(start).Nanoseconds() / int64(N)) } From 3d88a979050af77e757fcea0e4c48c3f04a2c7c1 Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Fri, 27 Dec 2024 16:37:22 +0100 Subject: [PATCH 04/21] improved console output clarity --- examples/multiparty/int_pir/main.go | 28 ++++++++++++++++------------ examples/multiparty/int_psi/main.go | 26 ++++++++++++++------------ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/examples/multiparty/int_pir/main.go b/examples/multiparty/int_pir/main.go index 736e660a..d191c5e3 100644 --- a/examples/multiparty/int_pir/main.go +++ b/examples/multiparty/int_pir/main.go @@ -146,7 +146,7 @@ func main() { // Create the N input parties and generate their secret keys and private inputs P := genparties(params, N, t) - l.Println("> Setup phase") // TODO: improve the logging output + l.Printf("========= Setup phase =========") // Step 1.a: Generation of the threshold secret key @@ -199,7 +199,7 @@ func main() { // Creates the evaluation key set from the rlk and the galKeys evk := rlwe.NewMemEvaluationKeySet(rlk, galKeys...) - l.Printf("\tSetup done (cloud: %s, party: %s)\n", + l.Printf("Setup done (cloud: %s, party: %s)\n", elapsedCKGCloud+elapsedRKGCloud+elapsedGKGCloud, elapsedCKGParty+elapsedRKGParty+elapsedGKGParty) @@ -212,7 +212,8 @@ func main() { } // Each party encrypts its input row under the collective public key - l.Println("> Database input phase") + l.Println("========= Database input phase ==========") + l.Println("> Input Encryption") encoder := bgv.NewEncoder(params) encryptor := rlwe.NewEncryptor(params, pk) pt := bgv.NewPlaintext(params, params.MaxLevel()) @@ -231,7 +232,7 @@ func main() { // Step 3: Query evaluation - l.Println("> Query evaluation phase") + l.Println("========= Query evaluation phase =========") queryIndex := 2 // Index of the ciphertext to retrieve. querier := P[0] // Party performing the query @@ -265,10 +266,11 @@ func main() { panic(err) } - l.Printf("\t%v...%v\n", res[:8], res[params.N()-8:]) l.Printf("> Finished (total cloud: %s, total party: %s)\n", elapsedCKGCloud+elapsedRKGCloud+elapsedGKGCloud+elapsedEncryptCloud+elapsedRequestCloudCPU+elapsedCKSCloud, elapsedCKGParty+elapsedRKGParty+elapsedGKGParty+elapsedEncryptParty+elapsedRequestParty+elapsedCKSParty+elapsedDecParty) + + l.Printf("Result: %v...%v\n", res[:8], res[params.N()-8:]) } func genparties(params bgv.Parameters, N, t int) []party { @@ -296,7 +298,7 @@ func genparties(params bgv.Parameters, N, t int) []party { func execCKGProtocol(params bgv.Parameters, crs sampling.PRNG, participants []party) *rlwe.PublicKey { - l.Println("> PublicKeyGen Phase") + l.Println("> Public Encryption Generation") // Creates a protocol type for the collective public key generation. // The type is stateless and can be used to generate as many public keys as needed. @@ -345,7 +347,7 @@ func execCKGProtocol(params bgv.Parameters, crs sampling.PRNG, participants []pa func execRKGProtocol(params bgv.Parameters, crs sampling.PRNG, participants []party) *rlwe.RelinearizationKey { - l.Println("> RelinearizationKeyGen Phase") + l.Println("> Relinearization Key Generation") // Creates a protocol type for the collective relinearization key generation. // The type is stateless and can be used to generate as many relinearization keys as needed. @@ -413,7 +415,7 @@ func execRKGProtocol(params bgv.Parameters, crs sampling.PRNG, participants []pa func execGTGProtocol(params bgv.Parameters, crs sampling.PRNG, galEls []uint64, participants []party) (galKeys []*rlwe.GaloisKey) { - l.Println("> GKG Phase") + l.Println("> Galois Automorphism-Keys Generation") // Creates a protocol type for the collective galois key generation. // The type is stateless and can be used to generate as many galois keys as needed. @@ -475,7 +477,7 @@ func execGTGProtocol(params bgv.Parameters, crs sampling.PRNG, galEls []uint64, func genQuery(params bgv.Parameters, queryIndex int, encoder *bgv.Encoder, encryptor *rlwe.Encryptor) *rlwe.Ciphertext { - l.Println("> QueryGen Phase") + l.Println("> Query Generation") // Creates a query vector from the query index queryCoeffs := make([]uint64, params.N()) @@ -493,11 +495,15 @@ func genQuery(params bgv.Parameters, queryIndex int, encoder *bgv.Encoder, encry check(err) }) + l.Printf("\tdone (cloud: %d, party %s)\n", 0, elapsedRequestParty) + return encQuery } func execRequest(params bgv.Parameters, NGoRoutine int, encQuery *rlwe.Ciphertext, encInputs []*rlwe.Ciphertext, evk rlwe.EvaluationKeySet) *rlwe.Ciphertext { + l.Println("> Query Evaluation") + // First, pre-compute the some plaintext masks for the query evaluation as: // plainmask[i] = encode([0, ..., 0, 1, 0, ..., 0]) (zero with a 1 at the i-th position). // In practice, the masks are pre-computed and reused accross queries. @@ -512,8 +518,6 @@ func execRequest(params bgv.Parameters, NGoRoutine int, encQuery *rlwe.Ciphertex } } - l.Println("> Request Phase") - // Buffer for the intermediate computation done by the helper encPartial := make([]*rlwe.Ciphertext, len(encInputs)) for i := range encPartial { @@ -622,7 +626,7 @@ func execRequest(params bgv.Parameters, NGoRoutine int, encQuery *rlwe.Ciphertex func execCKSProtocol(params bgv.Parameters, participants []party, receiver party, ctIn *rlwe.Ciphertext) *rlwe.Ciphertext { - l.Println("> KeySwitch Phase") + l.Println("> Query Result Re-Encryption") // Creates a protocol type for the collective key-switching protocol, with smudging distribution parameter of 2^30. // The type is stateless and can be used to generate as many key-switching keys as needed. diff --git a/examples/multiparty/int_psi/main.go b/examples/multiparty/int_psi/main.go index e7b51714..d93c583a 100644 --- a/examples/multiparty/int_psi/main.go +++ b/examples/multiparty/int_psi/main.go @@ -93,6 +93,7 @@ func main() { P := genparties(params, N) // Step 1: Setup of the collective public key and relinearization key + l.Printf("========= Setup Phase =========") pk := execCKGProtocol(params, crs, P) // generates the collective public key @@ -100,12 +101,12 @@ func main() { evk := rlwe.NewMemEvaluationKeySet(rlk) // creates the evaluation key from the relinearization key - l.Printf("\tdone (cloud: %s, party: %s)\n", - elapsedRKGCloud, elapsedRKGParty) - l.Printf("\tSetup done (cloud: %s, party: %s)\n", + l.Printf("Setup done (cloud: %s, party: %s)\n", elapsedRKGCloud+elapsedCKGCloud, elapsedRKGParty+elapsedCKGParty) // Step 2: Each party encrypts its input vector + l.Printf("========= Computation Phase =========") + expRes := genInputs(params, P) // generates the input vectors and the expected result encoder := bgv.NewEncoder(params) @@ -118,19 +119,20 @@ func main() { encOut := execPCKSProtocol(params, tpk, encRes, P) // Step 5: The target party decrypts the result with its secret key - l.Println("> ResulPlaintextModulus:") + l.Println("> Result Decryption") decryptor := rlwe.NewDecryptor(params, tsk) ptres := bgv.NewPlaintext(params, params.MaxLevel()) elapsedDecParty := runTimed(func() { decryptor.Decrypt(encOut, ptres) }) + l.Printf("\tdone (cloud: %s, party: %s)\n", time.Duration(0), elapsedDecParty) // Check the result res := make([]uint64, params.MaxSlots()) err = encoder.Decode(ptres, res) check(err) - l.Printf("\t%v\n", res[:16]) + l.Printf("\tResult: %v\n", res[:16]) for i := range expRes { if expRes[i] != res[i] { //l.Printf("\t%v\n", expRes) @@ -138,8 +140,8 @@ func main() { return } } - l.Println("\tcorrect") - l.Printf("> Finished (total cloud: %s, total party: %s)\n", + l.Println("\tCorrect") + l.Printf("Finished (total cloud: %s, total party: %s)\n", elapsedCKGCloud+elapsedRKGCloud+elapsedEncryptCloud+elapsedEvalCloud+elapsedPCKSCloud, elapsedCKGParty+elapsedRKGParty+elapsedEncryptParty+elapsedEvalParty+elapsedPCKSParty+elapsedDecParty) } @@ -157,7 +159,7 @@ func genparties(params bgv.Parameters, N int) []party { func execCKGProtocol(params bgv.Parameters, crs sampling.PRNG, P []party) *rlwe.PublicKey { - l.Println("> PublicKeyGen Phase") + l.Println("> Public Enryption Key Generation") // Creates a protocol type for the collective public key generation. // The type is stateless and can be used to generate as many public keys as needed. @@ -199,7 +201,7 @@ func execCKGProtocol(params bgv.Parameters, crs sampling.PRNG, P []party) *rlwe. func execRKGProtocol(params bgv.Parameters, crs sampling.PRNG, P []party) *rlwe.RelinearizationKey { - l.Println("> RelinearizationKeyGen Phase") + l.Println("> Relinearization Key Generation") // Creates a protocol type for the collective relinearization key generation. // The type is stateless and can be used to generate as many relinearization keys as needed. @@ -285,7 +287,7 @@ func inputPhase(params bgv.Parameters, P []party, pk *rlwe.PublicKey, encoder *b } // Each party encrypts its input vector - l.Println("> Encrypt Phase") + l.Println("> Input Encryption") encryptor := rlwe.NewEncryptor(params, pk) pt := bgv.NewPlaintext(params, params.MaxLevel()) @@ -358,7 +360,7 @@ func evalPhase(params bgv.Parameters, NGoRoutine int, encInputs []*rlwe.Cipherte // Start the tasks taskList := make([]*multTask, 0) - l.Println("> Eval Phase") + l.Println("> Circuit Evaluation") elapsedEvalCloud = runTimed(func() { for i, lvl := range encLvls[:len(encLvls)-1] { nextLvl := encLvls[i+1] @@ -392,7 +394,7 @@ func execPCKSProtocol(params bgv.Parameters, tpk *rlwe.PublicKey, encRes *rlwe.C // Collective key switching from the collective secret key to // the target public key - l.Println("> PublicKeySwitch Phase") + l.Println("> Output Re-Encryption") // Creates a protocol type for the collective public key switch. // The type is stateless and can be used to generate as many public key switches as needed. From 44bbe48d8f5d83b3e29a0dc9a5e79ba323091f58 Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Fri, 27 Dec 2024 16:44:21 +0100 Subject: [PATCH 05/21] multiparty.Combiner accepts the ParameterProvider interface --- examples/multiparty/int_pir/main.go | 2 +- multiparty/threshold.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/multiparty/int_pir/main.go b/examples/multiparty/int_pir/main.go index d191c5e3..8254bcb6 100644 --- a/examples/multiparty/int_pir/main.go +++ b/examples/multiparty/int_pir/main.go @@ -290,7 +290,7 @@ func genparties(params bgv.Parameters, N, t int) []party { shamirPts := getShamirPoints(P) for i := range P { - P[i].Combiner = multiparty.NewCombiner(params.Parameters, P[i].shamirPt, shamirPts, t) // TODO: NewCombiner takes interface + P[i].Combiner = multiparty.NewCombiner(params, P[i].shamirPt, shamirPts, t) } return P diff --git a/multiparty/threshold.go b/multiparty/threshold.go index 0283aa38..7b5d816d 100644 --- a/multiparty/threshold.go +++ b/multiparty/threshold.go @@ -114,9 +114,9 @@ func (thr Thresholdizer) AggregateShares(share1, share2 ShamirSecretShare, outSh // 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.Parameters, own ShamirPublicPoint, others []ShamirPublicPoint, threshold int) Combiner { +func NewCombiner(params rlwe.ParameterProvider, own ShamirPublicPoint, others []ShamirPublicPoint, threshold int) Combiner { cmb := Combiner{} - cmb.ringQP = params.RingQP() + cmb.ringQP = params.GetRLWEParameters().RingQP() cmb.threshold = threshold cmb.tmp1, cmb.tmp2 = cmb.ringQP.NewRNSScalar(), cmb.ringQP.NewRNSScalar() cmb.one = cmb.ringQP.NewRNSScalarFromUInt64(1) From f16844b97b1758a19c58bc88293777a26c3abb0d Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 5 Feb 2025 14:25:54 +0100 Subject: [PATCH 06/21] updated general examples README.md --- examples/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/README.md b/examples/README.md index 03bc6a16..72320521 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,19 +1,19 @@ -# Single Party Examples +# Single-party-HE Examples ## Applications Application examples are examples showcasing specific capabilities of the library on scaled-down real world scenarios. -### Binary +### Binary computations - `bin_blind_rotations`: an example showcasing the evaluation of the sign function using blind rotations on RLWE ciphertexts. -### Integers +### Integer computations - `int_ride_hailing`: an example on privacy preserving ride hailing. - `int_vectorized_OLE`: an example on vectorized oblivious linear evaluation using an RLWE trapdoor. -### Reals/Complexes +### Real/Complex computations - `reals_bootstrapping`: a series of examples showcasing the capabilities of the bootstrapping for fixed point arithmetic. - `basics`: an example showcasing the basic capabilities of the bootstrapping. @@ -38,11 +38,11 @@ Tutorials are examples showcasing the basic capabilities of the library. - `reals`: a tutorial on all the basic capabilities of the package `ckks`. -# Multi Party Examples +# Multiparty-HE Examples - - `int_pir`: an example showcasing multi-party private information retrieval. - - `int_psi`: an example showcasing multi-party private set intersection. - - `thresh_eval_key_gen`: an example showcasing multi-party threshold key-generation. + - `int_psi`: an example showcasing the $N$-out-of-$N$-threshold scheme in a *private set intersection* scenario. + - `int_pir`: an example showcasing the $T$-out-of-$N$-threshold scheme in a *private information retrieval* scenario. + - `thresh_eval_key_gen`: an example showcasing the generation of a large set of evaluation-keys in the $T$-out-of-$N$-threshold scheme. ## Parameters From b458feac421cdb3b92bda3e5dac7c2b938c10005 Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 5 Feb 2025 14:30:04 +0100 Subject: [PATCH 07/21] word wrapping the multiparty/README.md for readability --- multiparty/README.md | 479 ++++++++++++++++++++++++++++++------------- 1 file changed, 341 insertions(+), 138 deletions(-) diff --git a/multiparty/README.md b/multiparty/README.md index 1d1541e4..6702f18b 100644 --- a/multiparty/README.md +++ b/multiparty/README.md @@ -1,107 +1,216 @@ # Multiparty Schemes in Lattigo -The `multiparty` package implements several Multiparty Homomorphic Encryption (MHE) primitives based on Ring-Learning-with-Errors (RLWE). -It provides the implementation of two core schemes: +The `multiparty` package implements several Multiparty Homomorphic Encryption (MHE) +primitives based on Ring-Learning-with-Errors (RLWE). It provides the implementation of +two core schemes: 1. A $N$-out-of-$N$-threshold scheme 2. A $t$-out-of-$N$-threshold scheme -We provide more informations about these two core schemes below. Moreover, The `multiparty/mpbgv` and `multiparty/mpckks` packages provide scheme-specific functionalities (e.g., interactive bootstrapping) by implementing **threshold** versions of the single-party BFV/BGV and CKKS cryptosystems -found in the `schemes` package. -Note that, as for the single party schemes, most of the operations are generic and can be handled by the core schemes in the `multiparty` package. +We provide more informations about these two core schemes below. Moreover, The +`multiparty/mpbgv` and `multiparty/mpckks` packages provide scheme-specific +functionalities (e.g., interactive bootstrapping) by implementing **threshold** versions +of the single-party BFV/BGV and CKKS cryptosystems found in the `schemes` package. Note +that, as for the single party schemes, most of the operations are generic and can be +handled by the core schemes in the `multiparty` package. -The `multiparty` package and its sub-packages provide the **local steps** of the MHE-based Secure Multiparty Computation protocol (MHE-MPC), as described in ["Multiparty Homomorphic Encryption from Ring-Learning-with-Errors"](https://eprint.iacr.org/2020/304.pdf) by Mouchet et al. (2021) (which is an RLWE instantiation of the MPC protocol described in ["Multiparty computation with low communication, computation and interaction via threshold FHE"](https://eprint.iacr.org/2011/613.pdf) by Asharov et al. (2012)). +The `multiparty` package and its sub-packages provide the **local steps** of the MHE-based +Secure Multiparty Computation protocol (MHE-MPC), as described in ["Multiparty Homomorphic +Encryption from Ring-Learning-with-Errors"](https://eprint.iacr.org/2020/304.pdf) by +Mouchet et al. (2021) (which is an RLWE instantiation of the MPC protocol described in +["Multiparty computation with low communication, computation and interaction via threshold +FHE"](https://eprint.iacr.org/2011/613.pdf) by Asharov et al. (2012)). -Note that this package implements local operations only, hence does not assume or provide any network-layer protocol implementation. -However: -- All relevant types provide serialization methods implementing the `encoding.BinaryMarshaller` and `encoding.BinaryUnmarshaller` interfaces (see [https://pkg.go.dev/encoding](https://pkg.go.dev/encoding)) as well as the `io.WriterTo` and `io.ReaderFrom` interfaces (see [https://pkg.go.dev/io](https://pkg.go.dev/io)). -- The last section of this README provides a detailed overview of the MHE-MPC protocol, its different instantiations, and maps the protocol steps to the relevant Lattigo types and methods. -- The `examples/multiparty` folder contains example applications for simple MPC tasks. These examples are running all the parties in the same process, but demonstrate the use of the multiparty schemes in the MHE-MPC protocol. +Note that this package implements local operations only, hence does not assume or provide +any network-layer protocol implementation. However: +- All relevant types provide serialization methods implementing the + `encoding.BinaryMarshaller` and `encoding.BinaryUnmarshaller` interfaces (see + [https://pkg.go.dev/encoding](https://pkg.go.dev/encoding)) as well as the `io.WriterTo` + and `io.ReaderFrom` interfaces (see [https://pkg.go.dev/io](https://pkg.go.dev/io)). +- The last section of this README provides a detailed overview of the MHE-MPC protocol, + its different instantiations, and maps the protocol steps to the relevant Lattigo types + and methods. +- The `examples/multiparty` folder contains example applications for simple MPC tasks. + These examples are running all the parties in the same process, but demonstrate the use + of the multiparty schemes in the MHE-MPC protocol. ## The $N$-out-of-$N$-Threshold Scheme -Conceptually, the $N$-out-of-$N$-threshold scheme exploits the linearity of RLWE encryption to distribute the secret-key among $N$ parties. More specifically, the core cryptographic operation of (single-party) RLWE-based scheme is to compute functions of the form: $$F(a,s) = as+e$$ -over a ring $R$ where $a \in R$ is public, $s \in R$ is the secret-key of the scheme and $e \in R$ is a small ring element (sampled fresh for each function). For example, notice that generating an RLWE public-key corresponds to exactly this operation. The $N$-out-of-$N$-threshold scheme consists in splitting the secret-key $s$ into $N$ additive shares such that $s=\sum^N_{i=1} s_i$ and that $s_i$ is held by party $i$. -In this way, any secret-key operation (especially, decryption) requires the collaboration between **all** the $N$ parties. +Conceptually, the $N$-out-of-$N$-threshold scheme exploits the linearity of RLWE +encryption to distribute the secret-key among $N$ parties. More specifically, the core +cryptographic operation of (single-party) RLWE-based scheme is to compute functions of the +form: $$F(a,s) = as+e$$ over a ring $R$ where $a \in R$ is public, $s \in R$ is the +secret-key of the scheme and $e \in R$ is a small ring element (sampled fresh for each +function). For example, notice that generating an RLWE public-key corresponds to exactly +this operation. The $N$-out-of-$N$-threshold scheme consists in splitting the secret-key +$s$ into $N$ additive shares such that $s=\sum^N_{i=1} s_i$ and that $s_i$ is held by +party $i$. In this way, any secret-key operation (especially, decryption) requires the +collaboration between **all** the $N$ parties. -Exploiting the linear structure of $s$ and the (almost) linearity of $F$, the latter can be computed piece-wise by the parties, as: +Exploiting the linear structure of $s$ and the (almost) linearity of $F$, the latter can +be computed piece-wise by the parties, as: $$h_i = F(a, s_i) = as_i + e_i.$$ -Then $F(a,s)$ can be computed as $h=\sum^N_{i=1}h_i$. The output $h$ is the desired function, only with larger error. Moreover, the following features are relevant: +Then $F(a,s)$ can be computed as $h=\sum^N_{i=1}h_i$. The output $h$ is the desired +function, only with larger error. Moreover, the following features are relevant: -- Since $F(a, s_i)$ has the form of a RLWE sample, it can be publicly disclosed for aggregation. For example, the parties could use a helper for aggregating their shares $h_i$. -- Since the secret-key shares $s_i$ are random and never disclosed, they can be sampled locally by the parties (as normal RLWE secret-keys). Hence, no trusted dealer or private channels are necessary between the parties. -- Obtaining protocols for threshold generation of evaluation-keys and threshold decryption only requires to slightly adapt the function $F$ above. The overall linearity argument remains the same. -- The threshold decryption protocol can be generalized into a re-encryption protocol (where the actual decryption corresponds to re-encrypting towards the $s=0$ key). It is possible to re-encrypt towards a known public-key (for receivers external to the set of parties). -- The threshold decryption- and key-switching protocols require *smudging* parameter implementing the noise-flooding technique. This is a statistical security parameter ensuring that no secret value leaks to the decryption receiver through the error. See the SECURITY.md for more discussion on this aspect. +- Since $F(a, s_i)$ has the form of a RLWE sample, it can be publicly disclosed for + aggregation. For example, the parties could use a helper for aggregating their shares + $h_i$. +- Since the secret-key shares $s_i$ are random and never disclosed, they can be sampled + locally by the parties (as normal RLWE secret-keys). Hence, no trusted dealer or private + channels are necessary between the parties. +- Obtaining protocols for threshold generation of evaluation-keys and threshold decryption + only requires to slightly adapt the function $F$ above. The overall linearity argument + remains the same. +- The threshold decryption protocol can be generalized into a re-encryption protocol + (where the actual decryption corresponds to re-encrypting towards the $s=0$ key). It is + possible to re-encrypt towards a known public-key (for receivers external to the set of + parties). +- The threshold decryption- and key-switching protocols require *smudging* parameter + implementing the noise-flooding technique. This is a statistical security parameter + ensuring that no secret value leaks to the decryption receiver through the error. See + the SECURITY.md for more discussion on this aspect. ### Implementation -For each secret-key operation in the single party RLWE scheme, the `multiparty` package provides a "protocol" type implementing the local operations of the parties in the threshold scheme. More specifically, each protocol type provides a `GenShare(*rlwe.SecretKey, [...])` method for generating a party's share (i.e., computing $F(a, s_i)$ above) and a `AggregateShares(share1, share2 [...])` method to aggregate the shares. Moreover: +For each secret-key operation in the single party RLWE scheme, the `multiparty` package +provides a "protocol" type implementing the local operations of the parties in the +threshold scheme. More specifically, each protocol type provides a +`GenShare(*rlwe.SecretKey, [...])` method for generating a party's share (i.e., computing +$F(a, s_i)$ above) and a `AggregateShares(share1, share2 [...])` method to aggregate the +shares. Moreover: -- For some protocols, $a$ is a *common random polynomial* (CRP) sampled from a common random string (CRS). In this case, the protocol types provide a `SampleCRP(crs CRS)` method to obtain $a$. -- The protocol types also provide method for converting the final aggregated share into the protocol's output. E.g., the `PublicKeyGenProtocol` provides a `GenPublicKey(aggshare PublicKeyGenShare, [...])` method. +- For some protocols, $a$ is a *common random polynomial* (CRP) sampled from a common + random string (CRS). In this case, the protocol types provide a `SampleCRP(crs CRS)` + method to obtain $a$. +- The protocol types also provide method for converting the final aggregated share into + the protocol's output. E.g., the `PublicKeyGenProtocol` provides a + `GenPublicKey(aggshare PublicKeyGenShare, [...])` method. ## The $t$-out-of-$N$-Threshold Scheme -There might be settings where an $N$-out-of-$N$-threshold access-structure is too restrictive. For example, when $N$ is large, the probability of a single party being down at a given time increases. In cases where it can be assumed that the adversary cannot corrupt more than $t-1$ out of the $N$ parties, the $t$-out-of-$N$-threshold scheme can be employed to provide better liveness guarantees. More specifically, this scheme ensures that secret-key operations can be performed by any group of at least $t$ parties. +There might be settings where an $N$-out-of-$N$-threshold access-structure is too +restrictive. For example, when $N$ is large, the probability of a single party being down +at a given time increases. In cases where it can be assumed that the adversary cannot +corrupt more than $t-1$ out of the $N$ parties, the $t$-out-of-$N$-threshold scheme can be +employed to provide better liveness guarantees. More specifically, this scheme ensures +that secret-key operations can be performed by any group of at least $t$ parties. -Lattigo provides an implementation of the RLWE-based $t$-out-of-$N$-threshold scheme described in Mouchet et al.'s paper [An Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic Encryption](https://eprint.iacr.org/2022/780). Similarly to many threshold schemes, it relies on Shamir Secret Sharing to distribute the secret-key of the scheme. This is, the secret-key of the scheme is now of the form: +Lattigo provides an implementation of the RLWE-based $t$-out-of-$N$-threshold scheme +described in Mouchet et al.'s paper [An Efficient Threshold Access-Structure for +RLWE-Based Multiparty Homomorphic Encryption](https://eprint.iacr.org/2022/780). Similarly +to many threshold schemes, it relies on Shamir Secret Sharing to distribute the secret-key +of the scheme. This is, the secret-key of the scheme is now of the form: $$S(X) = s + s_1 X + s_2 X^2 + ... + s_t X^{t-1},$$ -i.e., a degree-$(t-1)$ polynomial in R[X] for which $s = S(0)$, and party $i$'s secret-key shares is distributed as $S(\alpha_i)$ for $(\alpha_1, \alpha_2, ... \alpha_N)$ $N$ distinct elements of $R$ forming an exceptional sequence. -Then, observe that $s$ can be reconstructed from any set of $t$ shares via Lagrange interpolation. For example, assuming reconstruction from the first $t$ shares: +i.e., a degree-$(t-1)$ polynomial in R[X] for which $s = S(0)$, and party $i$'s secret-key +shares is distributed as $S(\alpha_i)$ for $(\alpha_1, \alpha_2, ... \alpha_N)$ $N$ +distinct elements of $R$ forming an exceptional sequence. Then, observe that $s$ can be +reconstructed from any set of $t$ shares via Lagrange interpolation. For example, assuming +reconstruction from the first $t$ shares: -$$s = \sum^t_{i=1} S(\alpha_i) \cdot \prod^t_{\substack{j=1\\ i \neq j}} \frac{\alpha_j}{\alpha_j - \alpha_i} = \sum^t_{i=1} S(\alpha_i) \cdot l_i$$ +$$s = \sum^t_{i=1} S(\alpha_i) \cdot \prod^t_{\substack{j=1\\ i \neq j}} +\frac{\alpha_j}{\alpha_j - \alpha_i} = \sum^t_{i=1} S(\alpha_i) \cdot l_i$$ -Hence, the structure of $s$ is still linear, and we can again compute the $F$ function above piece-wise. However, the fact that $F(a,s)$ is only **approximately** linear in $s$ poses some challenge in performing the Shamir reconstruction in the "usual" way. -More specifically, we cannot compute $h_i = F(a, S(\alpha_i))$ locally and then combine the shares as $\sum^t_{i=1} h_i \cdot l_i$. This is because $l_i$ is a large $R$ element multiplying it with $S(0)a+e_i$ would result in a large error $e_i \cdot l_i$. +Hence, the structure of $s$ is still linear, and we can again compute the $F$ function +above piece-wise. However, the fact that $F(a,s)$ is only **approximately** linear in $s$ +poses some challenge in performing the Shamir reconstruction in the "usual" way. More +specifically, we cannot compute $h_i = F(a, S(\alpha_i))$ locally and then combine the +shares as $\sum^t_{i=1} h_i \cdot l_i$. This is because $l_i$ is a large $R$ element +multiplying it with $S(0)a+e_i$ would result in a large error $e_i \cdot l_i$. -The scheme of Mouchet et al. circumvents this issue by directly evaluating $h_i=F(a, S(\alpha_i) \cdot l_i)$ locally. Then the combination of the share is back to being a simple summation over $t$ shares: $h =\sum^t_{i=1} h_i$. This simple trick enables a very efficient and usable $t$-out-of-$N$ scheme: +The scheme of Mouchet et al. circumvents this issue by directly evaluating $h_i=F(a, +S(\alpha_i) \cdot l_i)$ locally. Then the combination of the share is back to being a +simple summation over $t$ shares: $h =\sum^t_{i=1} h_i$. This simple trick enables a very +efficient and usable $t$-out-of-$N$ scheme: -- $S$ can be generated non-interactively and without a trusted dealer by having each party generating a random degree-$(t-1)$ polynomial $S_i$ with $S_i(0) = s_i$, and by implicitly take $S=\sum^N_{i=1} S_i$. Observe then that $s = S(0) = \sum^N_{i=1} s_i$, which matches the $N$-out-of-$N$-threshold case. +- $S$ can be generated non-interactively and without a trusted dealer by having each party + generating a random degree-$(t-1)$ polynomial $S_i$ with $S_i(0) = s_i$, and by + implicitly take $S=\sum^N_{i=1} S_i$. Observe then that $s = S(0) = \sum^N_{i=1} s_i$, + which matches the $N$-out-of-$N$-threshold case. - Then, party $i$ can obtain its share $S(\alpha_i)$ by: - 1. having each party $j$ send $S_j(\alpha_i)$ to party $i$ (via a **private** channel), + 1. having each party $j$ send $S_j(\alpha_i)$ to party $i$ (via a **private** + channel), 2. having party $i$ compute $S(\alpha_i) = \sum^N_{j=1} S_j(\alpha_i)$. -- The above protocol is a single-round protocol, and state each party has to keep is then a single ring element $S(\alpha_i)$. -- When instantiated as above, the $t$-out-of-$N$-threshold scheme consists in a direct **extension** of the $N$-out-of-$N$-threshold scheme where: - 1. The parties operate a *re-sharing* of their secret-key $N$-out-of-$N$ secret-key share using the $t$-out-of-$N$ Shamir Secret Sharing scheme. - 2. The parties perform the secret-key operations (i.e., the protocols) in the same way as in the $N$-out-of-$N$-threshold scheme, yet among $t$ parties only and with $S(\alpha_i)\cdot l_i$ instead of $s_i$. +- The above protocol is a single-round protocol, and state each party has to keep is then + a single ring element $S(\alpha_i)$. +- When instantiated as above, the $t$-out-of-$N$-threshold scheme consists in a direct + **extension** of the $N$-out-of-$N$-threshold scheme where: + 1. The parties operate a *re-sharing* of their secret-key $N$-out-of-$N$ secret-key + share using the $t$-out-of-$N$ Shamir Secret Sharing scheme. + 2. The parties perform the secret-key operations (i.e., the protocols) in the same way + as in the $N$-out-of-$N$-threshold scheme, yet among $t$ parties only and with + $S(\alpha_i)\cdot l_i$ instead of $s_i$. -However, the scheme has the downside of requiring to know set of parties participating to a given secret-key operation (i.e., evaluation of $F$). This is because evaluating $F(a, S(\alpha_i) \cdot l_i$ requires each party $i$ to compute the Lagrange coefficient $l_i$, which depends on the participating set. -Another downside of this scheme is that it requires a round of private, pairwise message exchanges between the parties before the scheme can be used in the $t$-out-of-$N$ regime. +However, the scheme has the downside of requiring to know set of parties participating to +a given secret-key operation (i.e., evaluation of $F$). This is because evaluating $F(a, +S(\alpha_i) \cdot l_i$ requires each party $i$ to compute the Lagrange coefficient $l_i$, +which depends on the participating set. Another downside of this scheme is that it +requires a round of private, pairwise message exchanges between the parties before the +scheme can be used in the $t$-out-of-$N$ regime. ### Implementation -Thanks to the $t$-out-of-$N$-threshold scheme being a direct extension of the $N$-out-of-$N$-threshold scheme (see the discussion above), the implementation of the former consist of two new types: `Thresholdizer` and `Combiner`. +Thanks to the $t$-out-of-$N$-threshold scheme being a direct extension of the +$N$-out-of-$N$-threshold scheme (see the discussion above), the implementation of the +former consist of two new types: `Thresholdizer` and `Combiner`. -The `Thresholdizer` type implements the secret-key generation and re-sharing steps. This type corresponds to part 1. of the extension as described above. More specifically: -- `GenShamirPolynomial(threshold int, sk *rlwe.SecretKey)` generates the random degree-$(t-1)$ polynomial with `sk` as constant coefficient (i.e., $S_i$ above). -- `GenShamirSecretShare(recipient ShamirPublicPoint, [...])` generates re-sharing for a given recipient by evaluating the secret polynomial (i.e., $S_i(\alpha_j)$ above). -- `AggregateShares(share1, share2 ShamirSecretShare, [...])` aggregates two received shares (i.e., one addition step in computing $S(\alpha_i)$ above). +The `Thresholdizer` type implements the secret-key generation and re-sharing steps. This +type corresponds to part 1. of the extension as described above. More specifically: +- `GenShamirPolynomial(threshold int, sk *rlwe.SecretKey)` generates the random + degree-$(t-1)$ polynomial with `sk` as constant coefficient (i.e., $S_i$ above). +- `GenShamirSecretShare(recipient ShamirPublicPoint, [...])` generates re-sharing for a + given recipient by evaluating the secret polynomial (i.e., $S_i(\alpha_j)$ above). +- `AggregateShares(share1, share2 ShamirSecretShare, [...])` aggregates two received + shares (i.e., one addition step in computing $S(\alpha_i)$ above). -The `Combiner` type lets parties obtain $t$-out-of-$t$ additive shares from their $t$-out-of-$N$ Shamir shares. This type corresponds to part 2. of the extension as described above, and is called as a pre-processing before any secret-key operation performed in the $t$-out-of-$N$ regime. More specifically, the `Combiner.GenAdditiveShare` takes as input the $t$-out-of-$N$-threshold secret-share of the party ($S(\alpha_i)$ above) along with the set $L=\{\alpha_1, ..., \alpha_t\}$ of the $t$ parties participating to the protocol, and computes: +The `Combiner` type lets parties obtain $t$-out-of-$t$ additive shares from their +$t$-out-of-$N$ Shamir shares. This type corresponds to part 2. of the extension as +described above, and is called as a pre-processing before any secret-key operation +performed in the $t$-out-of-$N$ regime. More specifically, the `Combiner.GenAdditiveShare` +takes as input the $t$-out-of-$N$-threshold secret-share of the party ($S(\alpha_i)$ +above) along with the set $L=\{\alpha_1, ..., \alpha_t\}$ of the $t$ parties participating +to the protocol, and computes: -$$S(\alpha_i) \cdot l_i = S(\alpha_i) \cdot \prod_{\substack{\alpha_j \in L\\ \alpha_j \neq \alpha_i}} \frac{\alpha_j}{\alpha_j - \alpha_i}.$$ +$$S(\alpha_i) \cdot l_i = S(\alpha_i) \cdot \prod_{\substack{\alpha_j \in L\\ \alpha_j +\neq \alpha_i}} \frac{\alpha_j}{\alpha_j - \alpha_i}.$$ -Hence, from the share output by `GenAdditiveShare`, the usual protocols described for the $N$-out-of-$N$-threshold setting (see the previous section) can be used, yet with $N=t$. +Hence, from the share output by `GenAdditiveShare`, the usual protocols described for the +$N$-out-of-$N$-threshold setting (see the previous section) can be used, yet with $N=t$. ## MHE-MPC Protocol Overview -The protocol enables a group of N _input parties_ to compute a joint function over their private inputs under encryption and to provide a _receiver party_ with the result. -The protocol is generic and covers several system- and adversary-models: +The protocol enables a group of N _input parties_ to compute a joint function over their +private inputs under encryption and to provide a _receiver party_ with the result. The +protocol is generic and covers several system- and adversary-models: -**Peer-to-peer vs Cloud-assisted models**. The parties can execute the protocol in a peer-to-peer way or receive assistance from a third-party server (which is also considered an adversary). +**Peer-to-peer vs Cloud-assisted models**. The parties can execute the protocol in a +peer-to-peer way or receive assistance from a third-party server (which is also considered +an adversary). -**Internal vs External receivers**. _Receiver parties_ are the intended recipients of the computation result and can be either _internal_ or _external_ depending or whether they are or not input parties themselves, respectively. This distinction is important in practice, because external receivers do not need to be online (and even to be known) during the setup phase. +**Internal vs External receivers**. _Receiver parties_ are the intended recipients of the +computation result and can be either _internal_ or _external_ depending or whether they +are or not input parties themselves, respectively. This distinction is important in +practice, because external receivers do not need to be online (and even to be known) +during the setup phase. -**Anytrust vs Full-threshold Access-structure**. As for many MPC protocols, the assumption on the worst-case number of corrupted parties can be mapped in the cryptographic access-control mechanism (the _access structure_). The implemented MHE-MPC protocol is "anytrust" (N-out-of-N-threshold) by default, but can be relaxed to any positive threshold t-out-of-N (see Threshold Secret-Key Generation). +**Anytrust vs Full-threshold Access-structure**. As for many MPC protocols, the assumption +on the worst-case number of corrupted parties can be mapped in the cryptographic +access-control mechanism (the _access structure_). The implemented MHE-MPC protocol is +"anytrust" (N-out-of-N-threshold) by default, but can be relaxed to any positive threshold +t-out-of-N (see Threshold Secret-Key Generation). -**Passive vs Active Adversaries**. The implemented MHE-MPC protocol is secure against passive adversaries, and can in theory be extended to active security by requiring the parties to produce proofs that their shares are correctly computed for every round. Note that those proofs are not implemented in Lattigo. +**Passive vs Active Adversaries**. The implemented MHE-MPC protocol is secure against +passive adversaries, and can in theory be extended to active security by requiring the +parties to produce proofs that their shares are correctly computed for every round. Note +that those proofs are not implemented in Lattigo. -An execution of the MHE-based MPC protocol has two phases: the Setup phase and the Evaluation phase, each of which comprises a number of sub-protocols as depicted below (the details of each protocols are provided later). +An execution of the MHE-based MPC protocol has two phases: the Setup phase and the +Evaluation phase, each of which comprises a number of sub-protocols as depicted below (the +details of each protocols are provided later). 1. Setup Phase 1. Secret Keys Generation @@ -121,131 +230,225 @@ An execution of the MHE-based MPC protocol has two phases: the Setup phase and t ## MHE-MPC Protocol Steps Description -This section provides a description for each sub-protocol of the MHE-MPC protocol and provides pointers to the relevant Lattigo types and methods. -This description is a first draft and will evolve in the future. -For concrete code examples, see the `example/multiparty` folders. -For a more formal exposition, see ["Multiparty Homomorphic Encryption from Ring-Learning-with-Errors"](https://eprint.iacr.org/2020/304.pdf) and [An Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic Encryption](https://eprint.iacr.org/2022/780). +This section provides a description for each sub-protocol of the MHE-MPC protocol and +provides pointers to the relevant Lattigo types and methods. This description is a first +draft and will evolve in the future. For concrete code examples, see the +`example/multiparty` folders. For a more formal exposition, see ["Multiparty Homomorphic +Encryption from Ring-Learning-with-Errors"](https://eprint.iacr.org/2020/304.pdf) and [An +Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic +Encryption](https://eprint.iacr.org/2022/780). -The system model is abstracted by considering that the parties have access to a common public authenticated channel. -In the cloud-assisted setting, this public channel can be the helper cloud-server. -In the peer-to-peer setting, it could be a public broadcast channel. -We also assume that parties can communicate over private authenticated channels. +The system model is abstracted by considering that the parties have access to a common +public authenticated channel. In the cloud-assisted setting, this public channel can be +the helper cloud-server. In the peer-to-peer setting, it could be a public broadcast +channel. We also assume that parties can communicate over private authenticated channels. -Several protocols require the parties to have access to common uniformly random polynomials (CRP), which are sampled from a common random string (CRS). -This CRS is implemented as an interface type `multiparty.CRS` that can be read by the parties as a part of the protocols (see below). -The `multiparty.CRS` can be implemented by a `utils.KeyedPRNG` type for which all parties use the same key. +Several protocols require the parties to have access to common uniformly random +polynomials (CRP), which are sampled from a common random string (CRS). This CRS is +implemented as an interface type `multiparty.CRS` that can be read by the parties as a +part of the protocols (see below). The `multiparty.CRS` can be implemented by a +`utils.KeyedPRNG` type for which all parties use the same key. ### 1. Setup -In this phase, the parties generate the various keys that are required by the Evaluation phase. -Similarly to LSSS-based MPC protocols such as SPDZ, the setup phase does not depend on the input and can be pre-computed. -However, unlike LSSS-based MPC, the setup produces public-keys that can be re-used for an arbitrary number of evaluation phases. +In this phase, the parties generate the various keys that are required by the Evaluation +phase. Similarly to LSSS-based MPC protocols such as SPDZ, the setup phase does not depend +on the input and can be pre-computed. However, unlike LSSS-based MPC, the setup produces +public-keys that can be re-used for an arbitrary number of evaluation phases. #### 1.i Secret Keys Generation -The parties generate their individual secret-keys locally by using a `rlwe.KeyGenerator`; this provides them with a `rlwe.SecretKey` type. -See [core/rlwe/keygenerator.go](../core/rlwe/keygenerator.go) for further information on key-generation. +The parties generate their individual secret-keys locally by using a `rlwe.KeyGenerator`; +this provides them with a `rlwe.SecretKey` type. See +[core/rlwe/keygenerator.go](../core/rlwe/keygenerator.go) for further information on +key-generation. -The _ideal secret-key_ is implicitly defined as the sum of all secret-keys. -Hence, this secret-key enforces an _N-out-N_ access structure which requires all the parties to collaborate in a ciphertext decryption and thus tolerates N-1 dishonest parties. +The _ideal secret-key_ is implicitly defined as the sum of all secret-keys. Hence, this +secret-key enforces an _N-out-N_ access structure which requires all the parties to +collaborate in a ciphertext decryption and thus tolerates N-1 dishonest parties. #### 1.ii _[if t < N]_ Threshold Secret-Key Generation -For settings where an _N-out-N_ access structure is too restrictive (e.g., from an availability point of view), an optional Threshold Secret-Key Generation Protocol can be run to enable _t-out-of-N_ access-structures (hence tolerating t-1 dishonest parties). -The idea of this protocol is to apply Shamir Secret Sharing to the _ideal secret-key_ in such a way that any group of _t_ parties can reconstruct it. -This is achieved by a single-round protocol where each party applies Shamir Secret-Sharing to its own share of the _ideal secret-key_. +For settings where an _N-out-N_ access structure is too restrictive (e.g., from an +availability point of view), an optional Threshold Secret-Key Generation Protocol can be +run to enable _t-out-of-N_ access-structures (hence tolerating t-1 dishonest parties). The +idea of this protocol is to apply Shamir Secret Sharing to the _ideal secret-key_ in such +a way that any group of _t_ parties can reconstruct it. This is achieved by a single-round +protocol where each party applies Shamir Secret-Sharing to its own share of the _ideal +secret-key_. -We assume that each party is associated with a distinct `multiparty.ShamirPublicPoint` that is known to the other parties. +We assume that each party is associated with a distinct `multiparty.ShamirPublicPoint` +that is known to the other parties. -This protocol is implemented by the `multiparty.Thresholdizer` type and its steps are the following: -- Each party generates a `multiparty.ShamirPolynomial` by using the `Thresholdizer.GenShamirPolynomial` method, then generates a share of type `multiparty.ShamirSecretShare` for each of the other parties' `ShamirPublicPoint` by using the `Thresholdizer.GenShamirSecretShare`. -- Each party privately sends the respective `ShamirSecretShare` to each of the other parties. -- Each party aggregates all the `ShamirSecretShare`s it received using the `Thresholdizer.AggregateShares` method. +This protocol is implemented by the `multiparty.Thresholdizer` type and its steps are the +following: +- Each party generates a `multiparty.ShamirPolynomial` by using the + `Thresholdizer.GenShamirPolynomial` method, then generates a share of type + `multiparty.ShamirSecretShare` for each of the other parties' `ShamirPublicPoint` by + using the `Thresholdizer.GenShamirSecretShare`. +- Each party privately sends the respective `ShamirSecretShare` to each of the other + parties. +- Each party aggregates all the `ShamirSecretShare`s it received using the + `Thresholdizer.AggregateShares` method. Each party stores its aggregated `ShamirSecretShare` for later use. #### 1.iii Public Key Generation -The parties execute the collective public encryption-key generation protocol to obtain an encryption-key for the _ideal secret-key_. +The parties execute the collective public encryption-key generation protocol to obtain an +encryption-key for the _ideal secret-key_. -The protocol is implemented by the `multiparty.PublicKeyGenProtocol` type and its steps are as follows: -- Each party samples a common random polynomial (`multiparty.PublicKeyGenCRP`) from the CRS by using the `PublicKeyGenProtocol.SampleCRP` method. -- _[if t < N]_ Each party uses the `multiparty.Combiner.GenAdditiveShare` to obtain a t-out-of-t sharing and uses the result as its secret-key in the next step. -- Each party generates a share (`multiparty.PublicKeyGenShare`) from the CRP and their secret-key, by using the `PublicKeyGenProtocol.GenShare` method. -- Each party discloses its share over the public channel. The shares are aggregated with the `PublicKeyGenProtocol.AggregateShares` method. -- Each party can derive the public encryption-key (`rlwe.PublicKey`) by using the `PublicKeyGenProtocol.GenPublicKey` method. +The protocol is implemented by the `multiparty.PublicKeyGenProtocol` type and its steps +are as follows: +- Each party samples a common random polynomial (`multiparty.PublicKeyGenCRP`) from the + CRS by using the `PublicKeyGenProtocol.SampleCRP` method. +- _[if t < N]_ Each party uses the `multiparty.Combiner.GenAdditiveShare` to obtain a + t-out-of-t sharing and uses the result as its secret-key in the next step. +- Each party generates a share (`multiparty.PublicKeyGenShare`) from the CRP and their + secret-key, by using the `PublicKeyGenProtocol.GenShare` method. +- Each party discloses its share over the public channel. The shares are aggregated with + the `PublicKeyGenProtocol.AggregateShares` method. +- Each party can derive the public encryption-key (`rlwe.PublicKey`) by using the + `PublicKeyGenProtocol.GenPublicKey` method. -After the execution of this protocol, the parties have access to the collective public encryption-key, hence can provide their inputs to computations. +After the execution of this protocol, the parties have access to the collective public +encryption-key, hence can provide their inputs to computations. #### 1.iv Evaluation-Key Generation -In order to evaluate circuits on the collectively-encrypted inputs, the parties must generate the evaluation-keys that correspond to the operations they wish to support. -The generation of a relinearization-key, which enables compact homomorphic multiplication, is described below (see `multiparty.RelinearizationKeyGenProtocol`). -Additionally, and given that the circuit requires it, the parties can generate evaluation-keys to support rotations and other kinds of Galois automorphisms (see `multiparty.GaloisKeyGenProtocol` below). -Finally, it is possible to generate generic evaluation-keys to homomorphically re-encrypt a ciphertext from a secret-key to another (see `multiparty.EvaluationKeyGenProtocol`). +In order to evaluate circuits on the collectively-encrypted inputs, the parties must +generate the evaluation-keys that correspond to the operations they wish to support. The +generation of a relinearization-key, which enables compact homomorphic multiplication, is +described below (see `multiparty.RelinearizationKeyGenProtocol`). Additionally, and given +that the circuit requires it, the parties can generate evaluation-keys to support +rotations and other kinds of Galois automorphisms (see `multiparty.GaloisKeyGenProtocol` +below). Finally, it is possible to generate generic evaluation-keys to homomorphically +re-encrypt a ciphertext from a secret-key to another (see +`multiparty.EvaluationKeyGenProtocol`). ##### 1.iv.a Relinearization Key -This protocol provides the parties with a public relinearization-key (`rlwe.RelinearizationKey`) for the _ideal secret-key_. This public-key enables compact multiplications in RLWE schemes. Out of the described protocols in this package, this is the only two-round protocol. +This protocol provides the parties with a public relinearization-key +(`rlwe.RelinearizationKey`) for the _ideal secret-key_. This public-key enables compact +multiplications in RLWE schemes. Out of the described protocols in this package, this is +the only two-round protocol. -The protocol is implemented by the `multiparty.RelinearizationKeyGenProtocol` type and its steps are as follows: -- Each party samples a common random polynomial matrix (`multiparty.RelinearizationKeyGenCRP`) from the CRS by using the `RelinearizationKeyGenProtocol.SampleCRP` method. -- _[if t < N]_ Each party uses the `multiparty.Combiner.GenAdditiveShare` to obtain a t-out-of-t sharing and use the result as their secret-key in the next steps. -- Each party generates a share (`multiparty.RelinearizationKeyGenShare`) for the first protocol round by using the `RelinearizationKeyGenProtocol.GenShareRoundOne` method. This method also provides the party with an ephemeral secret-key (`rlwe.SecretKey`), which is required for the second round. -- Each party discloses its share for the first round over the public channel. The shares are aggregated with the `RelinearizationKeyGenProtocol.AggregateShares` method. -- Each party generates a share (also a `multiparty.RelinearizationKeyGenShare`) for the second protocol round by using the `RelinearizationKeyGenProtocol.GenShareRoundTwo` method. -- Each party discloses its share for the second round over the public channel. The shares are aggregated with the `RelinearizationKeyGenProtocol.AggregateShares` method. -- Each party can derive the public relinearization-key (`rlwe.RelinearizationKey`) by using the `RelinearizationKeyGenProtocol.GenRelinearizationKey` method. +The protocol is implemented by the `multiparty.RelinearizationKeyGenProtocol` type and +its steps are as follows: +- Each party samples a common random polynomial matrix + (`multiparty.RelinearizationKeyGenCRP`) from the CRS by using the + `RelinearizationKeyGenProtocol.SampleCRP` method. +- _[if t < N]_ Each party uses the `multiparty.Combiner.GenAdditiveShare` to obtain a + t-out-of-t sharing and use the result as their secret-key in the next steps. +- Each party generates a share (`multiparty.RelinearizationKeyGenShare`) for the first + protocol round by using the `RelinearizationKeyGenProtocol.GenShareRoundOne` method. + This method also provides the party with an ephemeral secret-key (`rlwe.SecretKey`), + which is required for the second round. +- Each party discloses its share for the first round over the public channel. The shares + are aggregated with the `RelinearizationKeyGenProtocol.AggregateShares` method. +- Each party generates a share (also a `multiparty.RelinearizationKeyGenShare`) for the + second protocol round by using the `RelinearizationKeyGenProtocol.GenShareRoundTwo` + method. +- Each party discloses its share for the second round over the public channel. The shares + are aggregated with the `RelinearizationKeyGenProtocol.AggregateShares` method. +- Each party can derive the public relinearization-key (`rlwe.RelinearizationKey`) by + using the `RelinearizationKeyGenProtocol.GenRelinearizationKey` method. ##### 1.iv.b Galois Keys -This protocol provides the parties with a public Galois-key (stored as `rlwe.GaloisKey` types) for the _ideal secret-key_. One Galois-key enables one specific Galois automorphism on the ciphertexts' slots. The protocol can be repeated to generate the keys for multiple automorphisms. +This protocol provides the parties with a public Galois-key (stored as `rlwe.GaloisKey` +types) for the _ideal secret-key_. One Galois-key enables one specific Galois automorphism +on the ciphertexts' slots. The protocol can be repeated to generate the keys for multiple +automorphisms. -The protocol is implemented by the `multiparty.GaloisKeyGenProtocol` type and its steps are as follows: -- Each party samples a common random polynomial matrix (`multiparty.GaloisKeyGenCRP`) from the CRS by using the `GaloisKeyGenProtocol.SampleCRP` method. -- _[if t < N]_ Each party uses the `multiparty.Combiner.GenAdditiveShare` to obtain a t-out-of-t sharing and uses the result as its secret-key in the next step. -- Each party generates a share (`multiparty.GaloisKeyGenShare`) by using `GaloisKeyGenProtocol.GenShare`. -- Each party discloses its `multiparty.GaloisKeyGenShare` over the public channel. The shares are aggregated with the `GaloisKeyGenProtocol.AggregateShares` method. -- Each party can derive the public Galois-key (`rlwe.GaloisKey`) from the final `GaloisKeyGenShare` by using the `GaloisKeyGenProtocol.AggregateShares` method. +The protocol is implemented by the `multiparty.GaloisKeyGenProtocol` type and its steps +are as follows: +- Each party samples a common random polynomial matrix (`multiparty.GaloisKeyGenCRP`) from + the CRS by using the `GaloisKeyGenProtocol.SampleCRP` method. +- _[if t < N]_ Each party uses the `multiparty.Combiner.GenAdditiveShare` to obtain a + t-out-of-t sharing and uses the result as its secret-key in the next step. +- Each party generates a share (`multiparty.GaloisKeyGenShare`) by using + `GaloisKeyGenProtocol.GenShare`. +- Each party discloses its `multiparty.GaloisKeyGenShare` over the public channel. The + shares are aggregated with the `GaloisKeyGenProtocol.AggregateShares` method. +- Each party can derive the public Galois-key (`rlwe.GaloisKey`) from the final + `GaloisKeyGenShare` by using the `GaloisKeyGenProtocol.AggregateShares` method. ##### 1.iv.c Other Evaluation Keys -This protocol provides the parties with a generic public Evaluation-key (stored as `rlwe.EvaluationKey` types) for the _ideal secret-key_. One Evaluation-key enables one specific public re-encryption from one key to another. +This protocol provides the parties with a generic public Evaluation-key (stored as +`rlwe.EvaluationKey` types) for the _ideal secret-key_. One Evaluation-key enables one +specific public re-encryption from one key to another. -The protocol is implemented by the `multiparty.EvaluationKeyGenProtocol` type and its steps are as follows: -- Each party samples a common random polynomial matrix (`multiparty.EvaluationKeyGenCRP`) from the CRS by using the `EvaluationKeyGenProtocol.SampleCRP` method. -- _[if t < N]_ Each party uses the `multiparty.Combiner.GenAdditiveShare` to obtain a t-out-of-t sharing and uses the result as its secret-key in the next step. -- Each party generates a share (`multiparty.EvaluationKeyGenShare`) by using `EvaluationKeyGenProtocol.GenShare`. -- Each party discloses its `multiparty.EvaluationKeyGenShare` over the public channel. The shares are aggregated with the `EvaluationKeyGenProtocol.AggregateShares` method. -- Each party can derive the public Evaluation-key (`rlwe.EvaluationKey`) from the final `EvaluationKeyGenShare` by using the `EvaluationKeyGenProtocol.AggregateShares` method. +The protocol is implemented by the `multiparty.EvaluationKeyGenProtocol` type and its +steps are as follows: +- Each party samples a common random polynomial matrix (`multiparty.EvaluationKeyGenCRP`) + from the CRS by using the `EvaluationKeyGenProtocol.SampleCRP` method. +- _[if t < N]_ Each party uses the `multiparty.Combiner.GenAdditiveShare` to obtain a + t-out-of-t sharing and uses the result as its secret-key in the next step. +- Each party generates a share (`multiparty.EvaluationKeyGenShare`) by using + `EvaluationKeyGenProtocol.GenShare`. +- Each party discloses its `multiparty.EvaluationKeyGenShare` over the public channel. The + shares are aggregated with the `EvaluationKeyGenProtocol.AggregateShares` method. +- Each party can derive the public Evaluation-key (`rlwe.EvaluationKey`) from the final + `EvaluationKeyGenShare` by using the `EvaluationKeyGenProtocol.AggregateShares` method. ### 2 Evaluation Phase #### 2.i Input step (Encryption Phase) -The parties provide their inputs for the computation during the Input Phase. -They use the collective encryption-key generated during the Setup Phase to encrypt their inputs, and send them through the public channel. -Since the collective encryption-key is a valid RLWE public encryption-key, it can be used directly with the single-party scheme. -Hence, the parties can use the `Encoder` and `Encryptor` interfaces of the desired encryption scheme (see [bgv.Encoder](../schemes/bgv/encoder.go), [ckks.Encoder](../schemes/ckks/encoder.go) and [rlwe.Encryptor](../core/rlwe/encryptor.go)). +The parties provide their inputs for the computation during the Input Phase. They use the +collective encryption-key generated during the Setup Phase to encrypt their inputs, and +send them through the public channel. Since the collective encryption-key is a valid RLWE +public encryption-key, it can be used directly with the single-party scheme. Hence, the +parties can use the `Encoder` and `Encryptor` interfaces of the desired encryption scheme +(see [bgv.Encoder](../schemes/bgv/encoder.go), [ckks.Encoder](../schemes/ckks/encoder.go) +and [rlwe.Encryptor](../core/rlwe/encryptor.go)). #### 2.ii Circuit Evaluation step -The computation of the desired function is performed homomorphically during the Evaluation Phase. -The step can be performed by the parties themselves or can be outsourced to a cloud-server. -Since the ciphertexts in the multiparty schemes are valid ciphertexts for the single-party ones, the homomorphic operation of the latter can be used directly (see [bgv.Evaluator](../schemes/bgv/evaluator.go) and [ckks.Evaluator](../schemes/ckks/evaluator.go)). +The computation of the desired function is performed homomorphically during the Evaluation +Phase. The step can be performed by the parties themselves or can be outsourced to a +cloud-server. Since the ciphertexts in the multiparty schemes are valid ciphertexts for +the single-party ones, the homomorphic operation of the latter can be used directly (see +[bgv.Evaluator](../schemes/bgv/evaluator.go) and +[ckks.Evaluator](../schemes/ckks/evaluator.go)). #### 2.iii Output step -The receiver(s) obtain their outputs through the final Output Phase, whose aim is to decrypt the ciphertexts resulting from the Evaluation Phase. -It is a two-step process with an optional pre-processing step when using the t-out-of-N access structure. -In the first step, Collective Key-Switching, the parties re-encrypt the desired ciphertext under the receiver's secret-key. -The second step is the local decryption of this re-encrypted ciphertext by the receiver. +The receiver(s) obtain their outputs through the final Output Phase, whose aim is to +decrypt the ciphertexts resulting from the Evaluation Phase. It is a two-step process with +an optional pre-processing step when using the t-out-of-N access structure. In the first +step, Collective Key-Switching, the parties re-encrypt the desired ciphertext under the +receiver's secret-key. The second step is the local decryption of this re-encrypted +ciphertext by the receiver. ##### 2.iii.a Collective Key-Switching -The parties perform a re-encryption of the desired ciphertext(s) from being encrypted under the _ideal secret-key_ to being encrypted under the receiver's secret-key. -There are two instantiations of the Collective Key-Switching protocol: -- Collective Key-Switching (KeySwitch), implemented as the `multiparty.KeySwitchProtocol` interface: it enables the parties to switch from their _ideal secret-key_ _s_ to another _ideal secret-key_ _s'_ when s' is collectively known by the parties. In the case where _s' = 0_, this is equivalent to a collective decryption protocol that can be used when the receiver is one of the input-parties. -- Collective Public-Key Switching (PublicKeySwitch), implemented as the `multiparty.PublicKeySwitchProtocol` interface, enables parties to switch from their _ideal secret-key_ _s_ to an arbitrary key _s'_ when provided with a public encryption-key for _s'_. Hence, this enables key-switching to a secret-key that is not known to the input parties, which enables external receivers. +The parties perform a re-encryption of the desired ciphertext(s) from being encrypted +under the _ideal secret-key_ to being encrypted under the receiver's secret-key. There are +two instantiations of the Collective Key-Switching protocol: +- Collective Key-Switching (KeySwitch), implemented as the `multiparty.KeySwitchProtocol` + interface: it enables the parties to switch from their _ideal secret-key_ _s_ to another + _ideal secret-key_ _s'_ when s' is collectively known by the parties. In the case where + _s' = 0_, this is equivalent to a collective decryption protocol that can be used when + the receiver is one of the input-parties. +- Collective Public-Key Switching (PublicKeySwitch), implemented as the + `multiparty.PublicKeySwitchProtocol` interface, enables parties to switch from their + _ideal secret-key_ _s_ to an arbitrary key _s'_ when provided with a public + encryption-key for _s'_. Hence, this enables key-switching to a secret-key that is not + known to the input parties, which enables external receivers. -While both protocol variants have slightly different local operations, their steps are the same: -- Each party generates a share (of type `multiparty.KeySwitchShare` or `multiparty.PublicKeySwitchShare`) with the `multiparty.(Public)KeySwitchProtocol.GenShare` method. This requires its own secret-key (a `rlwe.SecretKey`) as well as the destination key: its own share of the destination key (a `rlwe.SecretKey`) in KeySwitch or the destination public-key (a `rlwe.PublicKey`) in PublicKeySwitch. -- Each party discloses its `multiparty.KeySwitchShare` over the public channel. The shares are aggregated with the `(Public)KeySwitchProtocol.AggregateShares` method. -- From the aggregated `multiparty.KeySwitchShare`, any party can derive the ciphertext re-encrypted under _s'_ by using the `(Public)KeySwitchProtocol.KeySwitch` method. +While both protocol variants have slightly different local operations, their steps are the +same: +- Each party generates a share (of type `multiparty.KeySwitchShare` or + `multiparty.PublicKeySwitchShare`) with the + `multiparty.(Public)KeySwitchProtocol.GenShare` method. This requires its own secret-key + (a `rlwe.SecretKey`) as well as the destination key: its own share of the destination + key (a `rlwe.SecretKey`) in KeySwitch or the destination public-key (a `rlwe.PublicKey`) + in PublicKeySwitch. +- Each party discloses its `multiparty.KeySwitchShare` over the public channel. The shares + are aggregated with the `(Public)KeySwitchProtocol.AggregateShares` method. +- From the aggregated `multiparty.KeySwitchShare`, any party can derive the ciphertext + re-encrypted under _s'_ by using the `(Public)KeySwitchProtocol.KeySwitch` method. ##### 2.iii.b Decryption -Once the receivers have obtained the ciphertext re-encrypted under their respective keys, they can use the usual decryption algorithm of the single-party scheme to obtain the plaintext result (see [rlwe.Decryptor](../core/rlwe/decryptor.go). +Once the receivers have obtained the ciphertext re-encrypted under their respective keys, +they can use the usual decryption algorithm of the single-party scheme to obtain the +plaintext result (see [rlwe.Decryptor](../core/rlwe/decryptor.go)). ## References -1. Multiparty Homomorphic Encryption from Ring-Learning-With-Errors () -2. An Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic Encryption () +1. Multiparty Homomorphic Encryption from Ring-Learning-With-Errors + () +2. An Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic + Encryption () From 764d238c2a3b9011f48720a7525844a0ad284b2f Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 5 Feb 2025 14:39:20 +0100 Subject: [PATCH 08/21] removed todos according to technical committee discussion --- examples/multiparty/int_pir/main.go | 4 ++-- examples/multiparty/int_psi/main.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/multiparty/int_pir/main.go b/examples/multiparty/int_pir/main.go index 8254bcb6..b93c5135 100644 --- a/examples/multiparty/int_pir/main.go +++ b/examples/multiparty/int_pir/main.go @@ -178,7 +178,7 @@ func main() { for i := range P { P[i].tsk = thresholdizer.AllocateThresholdSecretShare() for j := range P { - err := thresholdizer.AggregateShares(tskShares[j][i], P[i].tsk, &P[i].tsk) // TODO wierd pointer arguments + err := thresholdizer.AggregateShares(tskShares[j][i], P[i].tsk, &P[i].tsk) check(err) } } @@ -320,7 +320,7 @@ func execCKGProtocol(params bgv.Parameters, crs sampling.PRNG, participants []pa elapsedCKGParty = runTimedParty(func() { for i, pi := range participants { // Generate the t-out-of-t secret key of the party within the group of participants - err := pi.Combiner.GenAdditiveShare(getShamirPoints(participants), pi.shamirPt, pi.tsk, tsks[i]) // TODO: discuss returning the key directly + err := pi.Combiner.GenAdditiveShare(getShamirPoints(participants), pi.shamirPt, pi.tsk, tsks[i]) check(err) // Generate the public key share of the party from the t-out-of-t secret key diff --git a/examples/multiparty/int_psi/main.go b/examples/multiparty/int_psi/main.go index d93c583a..5e0516c8 100644 --- a/examples/multiparty/int_psi/main.go +++ b/examples/multiparty/int_psi/main.go @@ -20,7 +20,6 @@ // 5. Decryption: The target party decrypts the result with its secret key. // // [Multiparty Homomorphic Encryption from Ring-Learning-With-Errors]: https://eprint.iacr.org/2020/304 -// TODO: do we want a README version of this docstring ? package main import ( From 2ccdf826ceffc2e7f9044b961c5bcf7ee61d3000 Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 5 Feb 2025 14:44:41 +0100 Subject: [PATCH 09/21] prevent bad display of hyphenized math symboles in READMEs --- examples/README.md | 6 ++--- multiparty/README.md | 54 ++++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/examples/README.md b/examples/README.md index 72320521..8c0edbee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -40,9 +40,9 @@ Tutorials are examples showcasing the basic capabilities of the library. # Multiparty-HE Examples - - `int_psi`: an example showcasing the $N$-out-of-$N$-threshold scheme in a *private set intersection* scenario. - - `int_pir`: an example showcasing the $T$-out-of-$N$-threshold scheme in a *private information retrieval* scenario. - - `thresh_eval_key_gen`: an example showcasing the generation of a large set of evaluation-keys in the $T$-out-of-$N$-threshold scheme. + - `int_psi`: an example showcasing the $N$-out-of- $N$-threshold scheme in a *private set intersection* scenario. + - `int_pir`: an example showcasing the $t$-out-of- $N$-threshold scheme in a *private information retrieval* scenario. + - `thresh_eval_key_gen`: an example showcasing the generation of a large set of evaluation-keys in the $t$-out-of- $N$-threshold scheme. ## Parameters diff --git a/multiparty/README.md b/multiparty/README.md index 6702f18b..5f416223 100644 --- a/multiparty/README.md +++ b/multiparty/README.md @@ -4,8 +4,8 @@ The `multiparty` package implements several Multiparty Homomorphic Encryption (M primitives based on Ring-Learning-with-Errors (RLWE). It provides the implementation of two core schemes: -1. A $N$-out-of-$N$-threshold scheme -2. A $t$-out-of-$N$-threshold scheme +1. A $N$-out-of- $N$-threshold scheme +2. A $t$-out-of- $N$-threshold scheme We provide more informations about these two core schemes below. Moreover, The `multiparty/mpbgv` and `multiparty/mpckks` packages provide scheme-specific @@ -34,15 +34,15 @@ any network-layer protocol implementation. However: These examples are running all the parties in the same process, but demonstrate the use of the multiparty schemes in the MHE-MPC protocol. -## The $N$-out-of-$N$-Threshold Scheme +## The $N$-out-of- $N$-Threshold Scheme -Conceptually, the $N$-out-of-$N$-threshold scheme exploits the linearity of RLWE +Conceptually, the $N$-out-of- $N$-threshold scheme exploits the linearity of RLWE encryption to distribute the secret-key among $N$ parties. More specifically, the core cryptographic operation of (single-party) RLWE-based scheme is to compute functions of the form: $$F(a,s) = as+e$$ over a ring $R$ where $a \in R$ is public, $s \in R$ is the secret-key of the scheme and $e \in R$ is a small ring element (sampled fresh for each function). For example, notice that generating an RLWE public-key corresponds to exactly -this operation. The $N$-out-of-$N$-threshold scheme consists in splitting the secret-key +this operation. The $N$-out-of- $N$-threshold scheme consists in splitting the secret-key $s$ into $N$ additive shares such that $s=\sum^N_{i=1} s_i$ and that $s_i$ is held by party $i$. In this way, any secret-key operation (especially, decryption) requires the collaboration between **all** the $N$ parties. @@ -89,16 +89,16 @@ shares. Moreover: the protocol's output. E.g., the `PublicKeyGenProtocol` provides a `GenPublicKey(aggshare PublicKeyGenShare, [...])` method. -## The $t$-out-of-$N$-Threshold Scheme +## The $t$-out-of- $N$-Threshold Scheme -There might be settings where an $N$-out-of-$N$-threshold access-structure is too +There might be settings where an $N$-out-of- $N$-threshold access-structure is too restrictive. For example, when $N$ is large, the probability of a single party being down at a given time increases. In cases where it can be assumed that the adversary cannot -corrupt more than $t-1$ out of the $N$ parties, the $t$-out-of-$N$-threshold scheme can be +corrupt more than $t-1$ out of the $N$ parties, the $t$-out-of- $N$-threshold scheme can be employed to provide better liveness guarantees. More specifically, this scheme ensures that secret-key operations can be performed by any group of at least $t$ parties. -Lattigo provides an implementation of the RLWE-based $t$-out-of-$N$-threshold scheme +Lattigo provides an implementation of the RLWE-based $t$-out-of- $N$-threshold scheme described in Mouchet et al.'s paper [An Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic Encryption](https://eprint.iacr.org/2022/780). Similarly to many threshold schemes, it relies on Shamir Secret Sharing to distribute the secret-key @@ -106,7 +106,7 @@ of the scheme. This is, the secret-key of the scheme is now of the form: $$S(X) = s + s_1 X + s_2 X^2 + ... + s_t X^{t-1},$$ -i.e., a degree-$(t-1)$ polynomial in R[X] for which $s = S(0)$, and party $i$'s secret-key +i.e., a degree- $(t-1)$ polynomial in R[X] for which $s = S(0)$, and party $i$'s secret-key shares is distributed as $S(\alpha_i)$ for $(\alpha_1, \alpha_2, ... \alpha_N)$ $N$ distinct elements of $R$ forming an exceptional sequence. Then, observe that $s$ can be reconstructed from any set of $t$ shares via Lagrange interpolation. For example, assuming @@ -125,24 +125,24 @@ multiplying it with $S(0)a+e_i$ would result in a large error $e_i \cdot l_i$. The scheme of Mouchet et al. circumvents this issue by directly evaluating $h_i=F(a, S(\alpha_i) \cdot l_i)$ locally. Then the combination of the share is back to being a simple summation over $t$ shares: $h =\sum^t_{i=1} h_i$. This simple trick enables a very -efficient and usable $t$-out-of-$N$ scheme: +efficient and usable $t$-out-of- $N$ scheme: - $S$ can be generated non-interactively and without a trusted dealer by having each party - generating a random degree-$(t-1)$ polynomial $S_i$ with $S_i(0) = s_i$, and by + generating a random degree- $(t-1)$ polynomial $S_i$ with $S_i(0) = s_i$, and by implicitly take $S=\sum^N_{i=1} S_i$. Observe then that $s = S(0) = \sum^N_{i=1} s_i$, - which matches the $N$-out-of-$N$-threshold case. + which matches the $N$-out-of- $N$-threshold case. - Then, party $i$ can obtain its share $S(\alpha_i)$ by: 1. having each party $j$ send $S_j(\alpha_i)$ to party $i$ (via a **private** channel), 2. having party $i$ compute $S(\alpha_i) = \sum^N_{j=1} S_j(\alpha_i)$. - The above protocol is a single-round protocol, and state each party has to keep is then a single ring element $S(\alpha_i)$. -- When instantiated as above, the $t$-out-of-$N$-threshold scheme consists in a direct - **extension** of the $N$-out-of-$N$-threshold scheme where: - 1. The parties operate a *re-sharing* of their secret-key $N$-out-of-$N$ secret-key - share using the $t$-out-of-$N$ Shamir Secret Sharing scheme. +- When instantiated as above, the $t$-out-of- $N$-threshold scheme consists in a direct + **extension** of the $N$-out-of- $N$-threshold scheme where: + 1. The parties operate a *re-sharing* of their secret-key $N$-out-of- $N$ secret-key + share using the $t$-out-of- $N$ Shamir Secret Sharing scheme. 2. The parties perform the secret-key operations (i.e., the protocols) in the same way - as in the $N$-out-of-$N$-threshold scheme, yet among $t$ parties only and with + as in the $N$-out-of- $N$-threshold scheme, yet among $t$ parties only and with $S(\alpha_i)\cdot l_i$ instead of $s_i$. However, the scheme has the downside of requiring to know set of parties participating to @@ -150,28 +150,28 @@ a given secret-key operation (i.e., evaluation of $F$). This is because evaluati S(\alpha_i) \cdot l_i$ requires each party $i$ to compute the Lagrange coefficient $l_i$, which depends on the participating set. Another downside of this scheme is that it requires a round of private, pairwise message exchanges between the parties before the -scheme can be used in the $t$-out-of-$N$ regime. +scheme can be used in the $t$-out-of- $N$ regime. ### Implementation -Thanks to the $t$-out-of-$N$-threshold scheme being a direct extension of the -$N$-out-of-$N$-threshold scheme (see the discussion above), the implementation of the +Thanks to the $t$-out-of- $N$-threshold scheme being a direct extension of the +$N$-out-of- $N$-threshold scheme (see the discussion above), the implementation of the former consist of two new types: `Thresholdizer` and `Combiner`. The `Thresholdizer` type implements the secret-key generation and re-sharing steps. This type corresponds to part 1. of the extension as described above. More specifically: - `GenShamirPolynomial(threshold int, sk *rlwe.SecretKey)` generates the random - degree-$(t-1)$ polynomial with `sk` as constant coefficient (i.e., $S_i$ above). + degree- $(t-1)$ polynomial with `sk` as constant coefficient (i.e., $S_i$ above). - `GenShamirSecretShare(recipient ShamirPublicPoint, [...])` generates re-sharing for a given recipient by evaluating the secret polynomial (i.e., $S_i(\alpha_j)$ above). - `AggregateShares(share1, share2 ShamirSecretShare, [...])` aggregates two received shares (i.e., one addition step in computing $S(\alpha_i)$ above). -The `Combiner` type lets parties obtain $t$-out-of-$t$ additive shares from their -$t$-out-of-$N$ Shamir shares. This type corresponds to part 2. of the extension as +The `Combiner` type lets parties obtain $t$-out-of- $t$ additive shares from their +$t$-out-of- $N$ Shamir shares. This type corresponds to part 2. of the extension as described above, and is called as a pre-processing before any secret-key operation -performed in the $t$-out-of-$N$ regime. More specifically, the `Combiner.GenAdditiveShare` -takes as input the $t$-out-of-$N$-threshold secret-share of the party ($S(\alpha_i)$ +performed in the $t$-out-of- $N$ regime. More specifically, the `Combiner.GenAdditiveShare` +takes as input the $t$-out-of- $N$-threshold secret-share of the party ($S(\alpha_i)$ above) along with the set $L=\{\alpha_1, ..., \alpha_t\}$ of the $t$ parties participating to the protocol, and computes: @@ -179,7 +179,7 @@ $$S(\alpha_i) \cdot l_i = S(\alpha_i) \cdot \prod_{\substack{\alpha_j \in L\\ \a \neq \alpha_i}} \frac{\alpha_j}{\alpha_j - \alpha_i}.$$ Hence, from the share output by `GenAdditiveShare`, the usual protocols described for the -$N$-out-of-$N$-threshold setting (see the previous section) can be used, yet with $N=t$. +$N$-out-of- $N$-threshold setting (see the previous section) can be used, yet with $N=t$. ## MHE-MPC Protocol Overview From 0fdd21f51c9e0f50d28df6701e5cbecd0ceb0dbb Mon Sep 17 00:00:00 2001 From: lehugueni Date: Wed, 12 Mar 2025 16:34:15 +0100 Subject: [PATCH 10/21] fix(bootstrapping): fix densetosparse keygen --- circuits/ckks/bootstrapping/keys.go | 2 +- core/rlwe/keygenerator.go | 3 ++- ring/operations.go | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/circuits/ckks/bootstrapping/keys.go b/circuits/ckks/bootstrapping/keys.go index 6c67be6b..cf86591a 100644 --- a/circuits/ckks/bootstrapping/keys.go +++ b/circuits/ckks/bootstrapping/keys.go @@ -138,7 +138,7 @@ func (p Parameters) genEncapsulationEvaluationKeysNew(skDense *rlwe.SecretKey) ( kgenDense := rlwe.NewKeyGenerator(params) skSparse := kgenSparse.GenSecretKeyWithHammingWeightNew(p.EphemeralSecretWeight) - EvkDenseToSparse = kgenDense.GenEvaluationKeyNew(skDense, skSparse) + EvkDenseToSparse = kgenSparse.GenEvaluationKeyNew(skDense, skSparse) EvkSparseToDense = kgenDense.GenEvaluationKeyNew(skSparse, skDense) return } diff --git a/core/rlwe/keygenerator.go b/core/rlwe/keygenerator.go index 9fc49c02..8f1a6ea3 100644 --- a/core/rlwe/keygenerator.go +++ b/core/rlwe/keygenerator.go @@ -231,7 +231,8 @@ func (kgen KeyGenerator) GenEvaluationKeysForRingSwapNew(skStd, skConjugateInvar // If the ringDegree(skOutput) > ringDegree(skInput), generates [-a*SkOut + w*P*skIn_{Y^{N/n}} + e, a] in X^{N}. // If the ringDegree(skOutput) < ringDegree(skInput), generates [-a*skOut_{Y^{N/n}} + w*P*skIn + e_{N}, a_{N}] in X^{N}. // Else generates [-a*skOut + w*P*skIn + e, a] in X^{N}. -// The output [EvaluationKey] is always given in max(N, n) and in the moduli of the output [EvaluationKey]. +// The output [EvaluationKey] is always given in max(N, n). +// If evkParams is void, the output [EvaluationKey] will be in the moduli given by the parameters stored in the [KeyGenerator] kgen. // When re-encrypting a [Ciphertext] from Y^{N/n} to X^{N}, the Ciphertext must first be mapped to X^{N} // using [SwitchCiphertextRingDegreeNTT](ctSmallDim, nil, ctLargeDim). // When re-encrypting a [Ciphertext] from X^{N} to Y^{N/n}, the output of the re-encryption is in still X^{N} and diff --git a/ring/operations.go b/ring/operations.go index 4fed1605..4ec76e84 100644 --- a/ring/operations.go +++ b/ring/operations.go @@ -379,7 +379,7 @@ func (r Ring) MulByVectorMontgomeryThenAddLazy(p1 Poly, vector []uint64, p2 Poly // MapSmallDimensionToLargerDimensionNTT maps Y = X^{N/n} -> X directly in the NTT domain func MapSmallDimensionToLargerDimensionNTT(polSmall, polLarge Poly) { gap := len(polLarge.Coeffs[0]) / len(polSmall.Coeffs[0]) - for j := range polSmall.Coeffs { + for j := 0; j <= Min(polSmall.Level(), polLarge.Level()); j++ { tmp0 := polSmall.Coeffs[j] tmp1 := polLarge.Coeffs[j] for i := range polSmall.Coeffs[0] { From 54658ee45383760e349ffe9672d6124c64b7a63d Mon Sep 17 00:00:00 2001 From: lehugueni Date: Thu, 13 Mar 2025 17:07:50 +0100 Subject: [PATCH 11/21] chore: use min --- ring/operations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ring/operations.go b/ring/operations.go index 4ec76e84..be6f8cf9 100644 --- a/ring/operations.go +++ b/ring/operations.go @@ -379,7 +379,7 @@ func (r Ring) MulByVectorMontgomeryThenAddLazy(p1 Poly, vector []uint64, p2 Poly // MapSmallDimensionToLargerDimensionNTT maps Y = X^{N/n} -> X directly in the NTT domain func MapSmallDimensionToLargerDimensionNTT(polSmall, polLarge Poly) { gap := len(polLarge.Coeffs[0]) / len(polSmall.Coeffs[0]) - for j := 0; j <= Min(polSmall.Level(), polLarge.Level()); j++ { + for j := 0; j <= min(polSmall.Level(), polLarge.Level()); j++ { tmp0 := polSmall.Coeffs[j] tmp1 := polLarge.Coeffs[j] for i := range polSmall.Coeffs[0] { From 37a34b4676e987795949a795f0ce9d0893763c96 Mon Sep 17 00:00:00 2001 From: lehugueni Date: Fri, 14 Mar 2025 13:01:28 +0100 Subject: [PATCH 12/21] docs(changelog): update changelog for release 6.1.1 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ebd3621..8c3cc072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog All notable changes to this library are documented in this file. +## [6.1.1] - 17.03.2025 +- Security fix: the evaluation key for switching from dense to sparse key must be generated at the lowest level. + ## [6.1.0] - 04.10.2024 - Update of `PrecisionStats` in `ckks/precision.go`: - The precision is now computed as the min/max/average/... of the log of the error (instead of the log of the min/max/average/... of the error). From cef0f57588b085204649cbe58c49cb9e0d9967cc Mon Sep 17 00:00:00 2001 From: lehugueni Date: Tue, 18 Mar 2025 08:23:16 +0100 Subject: [PATCH 13/21] docs(security.md): update security.md after 6.1.1 --- SECURITY.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 00bd7b18..0b6bacac 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,15 @@ # Report a Vulnerability To report a vulnerability please contact us directly using the following email: lattigo@tuneinsight.com. +# Past Vulnerabilities +### Wrong level for DenseToSparse Evaluation Key + - **Severity:** Low + - **Impact:** A security of 128-bit is not guaranteed if the `EvkDenseToSparse` evaluation key, which is part of the CKKS bootstrapping keys, is used. With the bootstrapping parameters proposed in Lattigo, the security falls to $\approx$ 106 bits of security. This means the vulnerability does not lead to any practical attack. However, anyone using CKKS bootstrapping should update their version to go back to 128-bit security. + - **Versions impacted:** `v5.0.0-v6.1.0` + - **Fixed in:** `v6.1.1` + - **Fix:** `EvkDenseToSparse` is generated at the correct level. + - **Reported by:** Noam Kleinburd **@NoamK-CR** + # Code Review Lattigo 2.0.0 was code-reviewed by ELCA in November 2020 and, within the allocated time for the code review, no critical or high-risk issues were found. From 94d9a00f821e9807eecd30d0bf46bd02cfffaa4c Mon Sep 17 00:00:00 2001 From: lehugueni Date: Tue, 18 Mar 2025 15:33:20 +0100 Subject: [PATCH 14/21] docs: update security.md --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 0b6bacac..a94a7de4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ To report a vulnerability please contact us directly using the following email: # Past Vulnerabilities ### Wrong level for DenseToSparse Evaluation Key - **Severity:** Low - - **Impact:** A security of 128-bit is not guaranteed if the `EvkDenseToSparse` evaluation key, which is part of the CKKS bootstrapping keys, is used. With the bootstrapping parameters proposed in Lattigo, the security falls to $\approx$ 106 bits of security. This means the vulnerability does not lead to any practical attack. However, anyone using CKKS bootstrapping should update their version to go back to 128-bit security. + - **Impact:** A security of 128-bit is not guaranteed if the `EvkDenseToSparse` evaluation key, which is part of the CKKS bootstrapping keys, is used. With the bootstrapping parameters proposed in Lattigo, the security falls to $\approx$ 106 bits of security. This means the vulnerability does not lead to any practical attack. However, anyone using CKKS bootstrapping should update their version of Lattigo and rotate their secret key to go back to 128-bit security. - **Versions impacted:** `v5.0.0-v6.1.0` - **Fixed in:** `v6.1.1` - **Fix:** `EvkDenseToSparse` is generated at the correct level. From 88a9689378f043bbf25d19d9fcacc0d7b5e324b2 Mon Sep 17 00:00:00 2001 From: lehugueni Date: Wed, 26 Mar 2025 08:28:35 +0100 Subject: [PATCH 15/21] docs: update security.md + readme.md with vul report --- README.md | 3 +++ SECURITY.md | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 2dc119e6..c876bdb6 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,9 @@ External pull requests only proposing small or trivial changes will be converted External contributions will require the signature of a Contributor License Agreement (CLA). You can contact us using the following email to request a copy of the CLA: [lattigo@tuneinsight.com](mailto:lattigo@tuneinsight.com). +## Vulnerability Reports +See [Report a Vulnerability](SECURITY.md#report-a-vulnerability). + ## Bug Reports Lattigo welcomes bug/regression reports of any kind that conform to the preset template, which is diff --git a/SECURITY.md b/SECURITY.md index a94a7de4..aeaf8f07 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,9 @@ # Report a Vulnerability To report a vulnerability please contact us directly using the following email: lattigo@tuneinsight.com. +Please include as much detail as possible in your report, including: + - Affected version. + - Steps to reproduce. + - Potential impact of the vulnerability. # Past Vulnerabilities ### Wrong level for DenseToSparse Evaluation Key From 067b9e8eb810b51b1fbaf43d1041f495805a379d Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 9 Apr 2025 17:30:21 +0200 Subject: [PATCH 16/21] Apply typo fixes from review Co-authored-by: lehugueni <12702835+lehugueni@users.noreply.github.com> --- examples/multiparty/int_pir/main.go | 10 +++++----- examples/multiparty/int_psi/main.go | 4 ++-- multiparty/README.md | 18 +++++++++--------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/multiparty/int_pir/main.go b/examples/multiparty/int_pir/main.go index b93c5135..a798e56e 100644 --- a/examples/multiparty/int_pir/main.go +++ b/examples/multiparty/int_pir/main.go @@ -1,9 +1,9 @@ -// This example demonstrates the use of the [multiparty] package to perform a basic N-party private information retrival (PIR) protocol. +// This example demonstrates the use of the [multiparty] package to perform a basic N-party private information retrieval (PIR) protocol. // This protocol relies on the t-out-of-N-threshold variant of the BGV scheme. // // In a nutshell, each party uploads an data row to a helper server, as an encrypted integer vector. The result is an encrypted database of N rows. At a later stage, -// a party queries a row in the database to the helper, without revealing the selector query index. The querier does so by encoding its query as -// a binary vector with a single 1 component corresponding to the row it wants to retrive, and send this encrypted vector to the helper. +// a party queries a row in the database to the helper, without revealing the selector query index. The querying party does so by encoding its query as +// a binary vector with a single 1 component corresponding to the row it wants to retrieve, and send this encrypted vector to the helper. // The helper can then compute the response (under encryption) by multiplying each row i of the database with the i-th query component, and summing // the resulting vectors together. Finally, the result can be decrypted by any group of t parties. // @@ -313,7 +313,7 @@ func execCKGProtocol(params bgv.Parameters, crs sampling.PRNG, participants []pa } ckgCombined := ckg.AllocateShare() // Allocate the memory for the combined share - // sample the common reference polynomial (crp) common reference string (crs) + // sample the common reference polynomial (crp) from the common reference string (crs) crp := ckg.SampleCRP(crs) // Generate the parties' shares @@ -504,7 +504,7 @@ func execRequest(params bgv.Parameters, NGoRoutine int, encQuery *rlwe.Ciphertex l.Println("> Query Evaluation") - // First, pre-compute the some plaintext masks for the query evaluation as: + // First, pre-compute the plaintext masks for the query evaluation as: // plainmask[i] = encode([0, ..., 0, 1, 0, ..., 0]) (zero with a 1 at the i-th position). // In practice, the masks are pre-computed and reused accross queries. encoder := bgv.NewEncoder(params) diff --git a/examples/multiparty/int_psi/main.go b/examples/multiparty/int_psi/main.go index 5e0516c8..f38d5586 100644 --- a/examples/multiparty/int_psi/main.go +++ b/examples/multiparty/int_psi/main.go @@ -171,7 +171,7 @@ func execCKGProtocol(params bgv.Parameters, crs sampling.PRNG, P []party) *rlwe. } ckgCombined := ckg.AllocateShare() // Allocate the memory for the combined share - // sample the common reference polynomial (crp) common reference string (crs) + // sample the common reference polynomial (crp) from the common reference string (crs) crp := ckg.SampleCRP(crs) // Generate the parties' shares @@ -217,7 +217,7 @@ func execRKGProtocol(params bgv.Parameters, crs sampling.PRNG, P []party) *rlwe. // Allocate the memory for the combined public shares _, rkgCombined1, rkgCombined2 := rkg.AllocateShare() - // Sample the common reference polynomial (crp) common reference string (crs) + // Sample the common reference polynomial (crp) from the common reference string (crs) crp := rkg.SampleCRP(crs) // The parties generate their shares for round one diff --git a/multiparty/README.md b/multiparty/README.md index 5f416223..120f6d9e 100644 --- a/multiparty/README.md +++ b/multiparty/README.md @@ -7,7 +7,7 @@ two core schemes: 1. A $N$-out-of- $N$-threshold scheme 2. A $t$-out-of- $N$-threshold scheme -We provide more informations about these two core schemes below. Moreover, The +We provide more informations about these two core schemes below. Moreover, the `multiparty/mpbgv` and `multiparty/mpckks` packages provide scheme-specific functionalities (e.g., interactive bootstrapping) by implementing **threshold** versions of the single-party BFV/BGV and CKKS cryptosystems found in the `schemes` package. Note @@ -123,7 +123,7 @@ shares as $\sum^t_{i=1} h_i \cdot l_i$. This is because $l_i$ is a large $R$ ele multiplying it with $S(0)a+e_i$ would result in a large error $e_i \cdot l_i$. The scheme of Mouchet et al. circumvents this issue by directly evaluating $h_i=F(a, -S(\alpha_i) \cdot l_i)$ locally. Then the combination of the share is back to being a +S(\alpha_i) \cdot l_i)$ locally. Then the combination of the shares is back to being a simple summation over $t$ shares: $h =\sum^t_{i=1} h_i$. This simple trick enables a very efficient and usable $t$-out-of- $N$ scheme: @@ -135,7 +135,7 @@ efficient and usable $t$-out-of- $N$ scheme: 1. having each party $j$ send $S_j(\alpha_i)$ to party $i$ (via a **private** channel), 2. having party $i$ compute $S(\alpha_i) = \sum^N_{j=1} S_j(\alpha_i)$. -- The above protocol is a single-round protocol, and state each party has to keep is then +- The above protocol is a single-round protocol, and the state each party has to keep is then a single ring element $S(\alpha_i)$. - When instantiated as above, the $t$-out-of- $N$-threshold scheme consists in a direct **extension** of the $N$-out-of- $N$-threshold scheme where: @@ -418,14 +418,14 @@ The parties perform a re-encryption of the desired ciphertext(s) from being encr under the _ideal secret-key_ to being encrypted under the receiver's secret-key. There are two instantiations of the Collective Key-Switching protocol: - Collective Key-Switching (KeySwitch), implemented as the `multiparty.KeySwitchProtocol` - interface: it enables the parties to switch from their _ideal secret-key_ _s_ to another - _ideal secret-key_ _s'_ when s' is collectively known by the parties. In the case where - _s' = 0_, this is equivalent to a collective decryption protocol that can be used when + interface: it enables the parties to switch from their _ideal secret-key_ $s$ to another + _ideal secret-key_ $s'$ when $s'$ is collectively known by the parties. In the case where + $s' = 0$, this is equivalent to a collective decryption protocol that can be used when the receiver is one of the input-parties. - Collective Public-Key Switching (PublicKeySwitch), implemented as the `multiparty.PublicKeySwitchProtocol` interface, enables parties to switch from their - _ideal secret-key_ _s_ to an arbitrary key _s'_ when provided with a public - encryption-key for _s'_. Hence, this enables key-switching to a secret-key that is not + _ideal secret-key_ $s$ to an arbitrary key $s'$ when provided with a public + encryption-key for $s'$. Hence, this enables key-switching to a secret-key that is not known to the input parties, which enables external receivers. While both protocol variants have slightly different local operations, their steps are the @@ -439,7 +439,7 @@ same: - Each party discloses its `multiparty.KeySwitchShare` over the public channel. The shares are aggregated with the `(Public)KeySwitchProtocol.AggregateShares` method. - From the aggregated `multiparty.KeySwitchShare`, any party can derive the ciphertext - re-encrypted under _s'_ by using the `(Public)KeySwitchProtocol.KeySwitch` method. + re-encrypted under $s'$ by using the `(Public)KeySwitchProtocol.KeySwitch` method. ##### 2.iii.b Decryption Once the receivers have obtained the ciphertext re-encrypted under their respective keys, From cfaf9ad27c3af1f2044eb022e8d553392b1891a3 Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 9 Apr 2025 17:45:20 +0200 Subject: [PATCH 17/21] added a precision on exceptional sequences --- multiparty/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/multiparty/README.md b/multiparty/README.md index 120f6d9e..46d1fcdb 100644 --- a/multiparty/README.md +++ b/multiparty/README.md @@ -108,9 +108,10 @@ $$S(X) = s + s_1 X + s_2 X^2 + ... + s_t X^{t-1},$$ i.e., a degree- $(t-1)$ polynomial in R[X] for which $s = S(0)$, and party $i$'s secret-key shares is distributed as $S(\alpha_i)$ for $(\alpha_1, \alpha_2, ... \alpha_N)$ $N$ -distinct elements of $R$ forming an exceptional sequence. Then, observe that $s$ can be -reconstructed from any set of $t$ shares via Lagrange interpolation. For example, assuming -reconstruction from the first $t$ shares: +distinct elements of $R$ forming an exceptional sequence (i.e., all their non-zero pairwise +differences are invertible in the ring). Then, observe that $s$ can be reconstructed from +any set of $t$ shares via Lagrange interpolation. For example, assuming reconstruction from +the first $t$ shares: $$s = \sum^t_{i=1} S(\alpha_i) \cdot \prod^t_{\substack{j=1\\ i \neq j}} \frac{\alpha_j}{\alpha_j - \alpha_i} = \sum^t_{i=1} S(\alpha_i) \cdot l_i$$ From c5571f6bce428aada821ac2fc54d2adb8de1937c Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 9 Apr 2025 17:59:24 +0200 Subject: [PATCH 18/21] fixed fmt --- examples/multiparty/int_pir/main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/multiparty/int_pir/main.go b/examples/multiparty/int_pir/main.go index 85e44040..3b9735c0 100644 --- a/examples/multiparty/int_pir/main.go +++ b/examples/multiparty/int_pir/main.go @@ -282,10 +282,9 @@ func genparties(params bgv.Parameters, N, t int) []party { P[i].sk = kgen.GenSecretKeyNew() - P[i].input = make([]uint64, params.N()) for j := range P[i].input { - /* #nosec G115 -- i cannot be negative */ + /* #nosec G115 -- i cannot be negative */ P[i].input[j] = uint64(i) } } From faf4bd6271c4cf70b4888ce6fe909057545e79d2 Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Wed, 9 Apr 2025 18:08:51 +0200 Subject: [PATCH 19/21] fixed gosec false positive --- examples/multiparty/int_pir/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/multiparty/int_pir/main.go b/examples/multiparty/int_pir/main.go index 3b9735c0..361e5022 100644 --- a/examples/multiparty/int_pir/main.go +++ b/examples/multiparty/int_pir/main.go @@ -278,6 +278,7 @@ func genparties(params bgv.Parameters, N, t int) []party { P := make([]party, N) kgen := rlwe.NewKeyGenerator(params) for i := range P { + /* #nosec G115 -- i cannot be negative */ P[i].shamirPt = multiparty.ShamirPublicPoint(i + 1) P[i].sk = kgen.GenSecretKeyNew() From 49ba2e624653a34e8c64d656418b80db19313efd Mon Sep 17 00:00:00 2001 From: Christian Mouchet Date: Mon, 14 Apr 2025 17:56:58 +0200 Subject: [PATCH 20/21] fixed math mode rendering of x-out-of-N --- examples/README.md | 6 +++--- multiparty/README.md | 48 ++++++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/examples/README.md b/examples/README.md index 8c0edbee..f1485fd0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -40,9 +40,9 @@ Tutorials are examples showcasing the basic capabilities of the library. # Multiparty-HE Examples - - `int_psi`: an example showcasing the $N$-out-of- $N$-threshold scheme in a *private set intersection* scenario. - - `int_pir`: an example showcasing the $t$-out-of- $N$-threshold scheme in a *private information retrieval* scenario. - - `thresh_eval_key_gen`: an example showcasing the generation of a large set of evaluation-keys in the $t$-out-of- $N$-threshold scheme. + - `int_psi`: an example showcasing the $N\text{-out-of-}N$-threshold scheme in a *private set intersection* scenario. + - `int_pir`: an example showcasing the $t\text{-out-of-}N$-threshold scheme in a *private information retrieval* scenario. + - `thresh_eval_key_gen`: an example showcasing the generation of a large set of evaluation-keys in the $t\text{-out-of-}N$-threshold scheme. ## Parameters diff --git a/multiparty/README.md b/multiparty/README.md index 46d1fcdb..48d70882 100644 --- a/multiparty/README.md +++ b/multiparty/README.md @@ -4,8 +4,8 @@ The `multiparty` package implements several Multiparty Homomorphic Encryption (M primitives based on Ring-Learning-with-Errors (RLWE). It provides the implementation of two core schemes: -1. A $N$-out-of- $N$-threshold scheme -2. A $t$-out-of- $N$-threshold scheme +1. A $N\text{-out-of-}N$-threshold scheme +2. A $t\text{-out-of-}N$-threshold scheme We provide more informations about these two core schemes below. Moreover, the `multiparty/mpbgv` and `multiparty/mpckks` packages provide scheme-specific @@ -34,15 +34,15 @@ any network-layer protocol implementation. However: These examples are running all the parties in the same process, but demonstrate the use of the multiparty schemes in the MHE-MPC protocol. -## The $N$-out-of- $N$-Threshold Scheme +## The $N\text{-out-of-}N$-Threshold Scheme -Conceptually, the $N$-out-of- $N$-threshold scheme exploits the linearity of RLWE +Conceptually, the $N\text{-out-of-}N$-threshold scheme exploits the linearity of RLWE encryption to distribute the secret-key among $N$ parties. More specifically, the core cryptographic operation of (single-party) RLWE-based scheme is to compute functions of the form: $$F(a,s) = as+e$$ over a ring $R$ where $a \in R$ is public, $s \in R$ is the secret-key of the scheme and $e \in R$ is a small ring element (sampled fresh for each function). For example, notice that generating an RLWE public-key corresponds to exactly -this operation. The $N$-out-of- $N$-threshold scheme consists in splitting the secret-key +this operation. The $N\text{-out-of-}N$-threshold scheme consists in splitting the secret-key $s$ into $N$ additive shares such that $s=\sum^N_{i=1} s_i$ and that $s_i$ is held by party $i$. In this way, any secret-key operation (especially, decryption) requires the collaboration between **all** the $N$ parties. @@ -89,16 +89,16 @@ shares. Moreover: the protocol's output. E.g., the `PublicKeyGenProtocol` provides a `GenPublicKey(aggshare PublicKeyGenShare, [...])` method. -## The $t$-out-of- $N$-Threshold Scheme +## The $t\text{-out-of-}N$-Threshold Scheme -There might be settings where an $N$-out-of- $N$-threshold access-structure is too +There might be settings where an $N\text{-out-of-}N$-threshold access-structure is too restrictive. For example, when $N$ is large, the probability of a single party being down at a given time increases. In cases where it can be assumed that the adversary cannot -corrupt more than $t-1$ out of the $N$ parties, the $t$-out-of- $N$-threshold scheme can be +corrupt more than $t-1$ out of the $N$ parties, the $t\text{-out-of-}N$-threshold scheme can be employed to provide better liveness guarantees. More specifically, this scheme ensures that secret-key operations can be performed by any group of at least $t$ parties. -Lattigo provides an implementation of the RLWE-based $t$-out-of- $N$-threshold scheme +Lattigo provides an implementation of the RLWE-based $t\text{-out-of-}N$-threshold scheme described in Mouchet et al.'s paper [An Efficient Threshold Access-Structure for RLWE-Based Multiparty Homomorphic Encryption](https://eprint.iacr.org/2022/780). Similarly to many threshold schemes, it relies on Shamir Secret Sharing to distribute the secret-key @@ -126,24 +126,24 @@ multiplying it with $S(0)a+e_i$ would result in a large error $e_i \cdot l_i$. The scheme of Mouchet et al. circumvents this issue by directly evaluating $h_i=F(a, S(\alpha_i) \cdot l_i)$ locally. Then the combination of the shares is back to being a simple summation over $t$ shares: $h =\sum^t_{i=1} h_i$. This simple trick enables a very -efficient and usable $t$-out-of- $N$ scheme: +efficient and usable $t\text{-out-of-}N$ scheme: - $S$ can be generated non-interactively and without a trusted dealer by having each party generating a random degree- $(t-1)$ polynomial $S_i$ with $S_i(0) = s_i$, and by implicitly take $S=\sum^N_{i=1} S_i$. Observe then that $s = S(0) = \sum^N_{i=1} s_i$, - which matches the $N$-out-of- $N$-threshold case. + which matches the $N\text{-out-of-}N$-threshold case. - Then, party $i$ can obtain its share $S(\alpha_i)$ by: 1. having each party $j$ send $S_j(\alpha_i)$ to party $i$ (via a **private** channel), 2. having party $i$ compute $S(\alpha_i) = \sum^N_{j=1} S_j(\alpha_i)$. - The above protocol is a single-round protocol, and the state each party has to keep is then a single ring element $S(\alpha_i)$. -- When instantiated as above, the $t$-out-of- $N$-threshold scheme consists in a direct - **extension** of the $N$-out-of- $N$-threshold scheme where: - 1. The parties operate a *re-sharing* of their secret-key $N$-out-of- $N$ secret-key - share using the $t$-out-of- $N$ Shamir Secret Sharing scheme. +- When instantiated as above, the $t\text{-out-of-}N$-threshold scheme consists in a direct + **extension** of the $N\text{-out-of-}N$-threshold scheme where: + 1. The parties operate a *re-sharing* of their secret-key $N\text{-out-of-}N$ secret-key + share using the $t\text{-out-of-}N$ Shamir Secret Sharing scheme. 2. The parties perform the secret-key operations (i.e., the protocols) in the same way - as in the $N$-out-of- $N$-threshold scheme, yet among $t$ parties only and with + as in the $N\text{-out-of-}N$-threshold scheme, yet among $t$ parties only and with $S(\alpha_i)\cdot l_i$ instead of $s_i$. However, the scheme has the downside of requiring to know set of parties participating to @@ -151,12 +151,12 @@ a given secret-key operation (i.e., evaluation of $F$). This is because evaluati S(\alpha_i) \cdot l_i$ requires each party $i$ to compute the Lagrange coefficient $l_i$, which depends on the participating set. Another downside of this scheme is that it requires a round of private, pairwise message exchanges between the parties before the -scheme can be used in the $t$-out-of- $N$ regime. +scheme can be used in the $t\text{-out-of-}N$ regime. ### Implementation -Thanks to the $t$-out-of- $N$-threshold scheme being a direct extension of the -$N$-out-of- $N$-threshold scheme (see the discussion above), the implementation of the +Thanks to the $t\text{-out-of-}N$-threshold scheme being a direct extension of the +$N\text{-out-of-}N$-threshold scheme (see the discussion above), the implementation of the former consist of two new types: `Thresholdizer` and `Combiner`. The `Thresholdizer` type implements the secret-key generation and re-sharing steps. This @@ -168,11 +168,11 @@ type corresponds to part 1. of the extension as described above. More specifical - `AggregateShares(share1, share2 ShamirSecretShare, [...])` aggregates two received shares (i.e., one addition step in computing $S(\alpha_i)$ above). -The `Combiner` type lets parties obtain $t$-out-of- $t$ additive shares from their -$t$-out-of- $N$ Shamir shares. This type corresponds to part 2. of the extension as +The `Combiner` type lets parties obtain $t\text{-out-of-}t$ additive shares from their +$t\text{-out-of-}N$ Shamir shares. This type corresponds to part 2. of the extension as described above, and is called as a pre-processing before any secret-key operation -performed in the $t$-out-of- $N$ regime. More specifically, the `Combiner.GenAdditiveShare` -takes as input the $t$-out-of- $N$-threshold secret-share of the party ($S(\alpha_i)$ +performed in the $t\text{-out-of-}N$ regime. More specifically, the `Combiner.GenAdditiveShare` +takes as input the $t\text{-out-of-}N$-threshold secret-share of the party ($S(\alpha_i)$ above) along with the set $L=\{\alpha_1, ..., \alpha_t\}$ of the $t$ parties participating to the protocol, and computes: @@ -180,7 +180,7 @@ $$S(\alpha_i) \cdot l_i = S(\alpha_i) \cdot \prod_{\substack{\alpha_j \in L\\ \a \neq \alpha_i}} \frac{\alpha_j}{\alpha_j - \alpha_i}.$$ Hence, from the share output by `GenAdditiveShare`, the usual protocols described for the -$N$-out-of- $N$-threshold setting (see the previous section) can be used, yet with $N=t$. +$N\text{-out-of-}N$-threshold setting (see the previous section) can be used, yet with $N=t$. ## MHE-MPC Protocol Overview From 781c5d346073422e16a4826700ff8a7864ea939e Mon Sep 17 00:00:00 2001 From: lehugueni Date: Tue, 15 Apr 2025 15:17:07 +0200 Subject: [PATCH 21/21] style: apply feedback to changelog.md and security.md --- CHANGELOG.md | 2 +- SECURITY.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8317703d..94fe1280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog All notable changes to this library are documented in this file. -## [7.x.x] - xx.xx.xxxx +## [Unreleased] - Refactoring of the InnerSum methods: - `rlwe.Evaluator.InnerSum` has been replaced by `rlwe.Evaluator.PartialTracesSum`, which applies the automorphisms that correspond to rotations at the scheme level (and sum the results). - Introduction of the `bgv.Evaluator.InnerSum` and `ckks.Evaluator.InnerSum` methods, which have the same behaviour as the old `InnerSum` method for parameters `n` and `batchSize` s.t. `0 < n*batchSize <= ctIn.Slots()` divides the number of slots. Parameters not satisfying these conditions are rejected. diff --git a/SECURITY.md b/SECURITY.md index aeaf8f07..47746325 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,9 +6,9 @@ Please include as much detail as possible in your report, including: - Potential impact of the vulnerability. # Past Vulnerabilities -### Wrong level for DenseToSparse Evaluation Key +### Wrong level for DenseToSparse Evaluation Key (04.2025) - **Severity:** Low - - **Impact:** A security of 128-bit is not guaranteed if the `EvkDenseToSparse` evaluation key, which is part of the CKKS bootstrapping keys, is used. With the bootstrapping parameters proposed in Lattigo, the security falls to $\approx$ 106 bits of security. This means the vulnerability does not lead to any practical attack. However, anyone using CKKS bootstrapping should update their version of Lattigo and rotate their secret key to go back to 128-bit security. + - **Impact:** A security of 128-bit is not guaranteed if the `EvkDenseToSparse` evaluation key, which is part of the CKKS bootstrapping keys, is used. With the bootstrapping parameters proposed in Lattigo, the security falls to $\approx$ 106 bits of security. This means the vulnerability should not lead to any practical attack. However, anyone using CKKS bootstrapping should update their version of Lattigo and rotate their secret key to go back to 128-bit security. - **Versions impacted:** `v5.0.0-v6.1.0` - **Fixed in:** `v6.1.1` - **Fix:** `EvkDenseToSparse` is generated at the correct level.