From 2da1be7e1591de3d1993665a0990aff67a1b4295 Mon Sep 17 00:00:00 2001 From: lehugueni Date: Thu, 20 Feb 2025 09:48:17 +0100 Subject: [PATCH] refactor: make keyedprng thread-safe, restrict usage + add warning --- core/rlwe/encryptor.go | 11 ++++---- core/rlwe/keygenerator.go | 2 +- core/rlwe/rlwe_test.go | 6 ++--- ring/ringqp/samplers.go | 9 +------ ring/sampler.go | 2 ++ ring/sampler_gaussian.go | 1 + ring/sampler_uniform.go | 10 +------ utils/sampling/prng.go | 56 +++++++++++++++++++-------------------- 8 files changed, 42 insertions(+), 55 deletions(-) diff --git a/core/rlwe/encryptor.go b/core/rlwe/encryptor.go index 85986f1d..45d9c8cc 100644 --- a/core/rlwe/encryptor.go +++ b/core/rlwe/encryptor.go @@ -94,9 +94,10 @@ func newEncryptor(params Parameters) *Encryptor { } } -// NewTestEncryptorWithPRNG creates a new [Encryptor] that uses the provided prng for randomness. +// newTestEncryptorWithKeyedPRNG creates a new [Encryptor] that uses the provided prng for randomness. // CAUTION: THIS FUNCTION SHOULD BE USED FOR TESTING PURPOSES ONLY. -func NewTestEncryptorWithPRNG(params ParameterProvider, key EncryptionKey, prng sampling.PRNG) *Encryptor { +// WARNING: The resulting encryptor is not meant to be used concurrently. +func newTestEncryptorWithKeyedPRNG(params ParameterProvider, key EncryptionKey, prng *sampling.KeyedPRNG) *Encryptor { p := *params.GetRLWEParameters() enc := NewEncryptor(params, key) @@ -455,10 +456,10 @@ func (enc Encryptor) encryptZeroSkFromC1QP(sk *SecretKey, ct Element[ringqp.Poly return } -// WithPRNG returns this encryptor with prng as its source of randomness for the uniform +// withKeyedUniformSampling returns this encryptor with a keyed prng as its source of randomness for the uniform // element c1. -// The returned encryptor isn't safe to use concurrently with the original encryptor. -func (enc Encryptor) WithPRNG(prng sampling.PRNG) *Encryptor { +// The returned encryptor is not thread safe (sampling will not be deterministic). +func (enc Encryptor) withKeyedUniformSampling(prng *sampling.KeyedPRNG) *Encryptor { enc.uniformSampler = ringqp.NewUniformSampler(prng, *enc.params.RingQP()) return &enc } diff --git a/core/rlwe/keygenerator.go b/core/rlwe/keygenerator.go index 5249d7c8..8f293b20 100644 --- a/core/rlwe/keygenerator.go +++ b/core/rlwe/keygenerator.go @@ -291,7 +291,7 @@ func (kgen KeyGenerator) genEvaluationKey(skIn ring.Poly, skOut ringqp.Poly, evk panic(fmt.Errorf("sampling.NewKeyedPRNG: %w", err)) } - enc = enc.WithPRNG(sampler) + enc = enc.withKeyedUniformSampling(sampler) } // Samples an encryption of zero for each element of the EvaluationKey. diff --git a/core/rlwe/rlwe_test.go b/core/rlwe/rlwe_test.go index ae2985fd..733d4ad9 100644 --- a/core/rlwe/rlwe_test.go +++ b/core/rlwe/rlwe_test.go @@ -318,7 +318,7 @@ func NewDeterministicTestContext(params Parameters, seedKeyGen, seedEnc []byte) panic(fmt.Errorf("NewDeterministicTestContext: failed to make prngEnc %w", err)) } kgen := NewKeyGenerator(params) - kgenEncryptor := NewTestEncryptorWithPRNG(params, nil, prngKGen) + kgenEncryptor := newTestEncryptorWithKeyedPRNG(params, nil, prngKGen) kgen.Encryptor = kgenEncryptor sk := kgen.GenSecretKeyNew() @@ -327,7 +327,7 @@ func NewDeterministicTestContext(params Parameters, seedKeyGen, seedEnc []byte) eval := NewEvaluator(params, nil) - enc := NewTestEncryptorWithPRNG(params, sk, prngEnc) + enc := newTestEncryptorWithKeyedPRNG(params, sk, prngEnc) dec := NewDecryptor(params, sk) @@ -653,7 +653,7 @@ func testEncryptor(tc *TestContext, level, bpw2 int, t *testing.T) { prng1, _ := sampling.NewKeyedPRNG([]byte{'a', 'b', 'c'}) prng2, _ := sampling.NewKeyedPRNG([]byte{'a', 'b', 'c'}) - enc.WithPRNG(prng1).Encrypt(pt, ct) + enc.withKeyedUniformSampling(prng1).Encrypt(pt, ct) samplerQ := ring.NewUniformSampler(prng2, ringQ) diff --git a/ring/ringqp/samplers.go b/ring/ringqp/samplers.go index cd0d6c0d..a4b01d28 100644 --- a/ring/ringqp/samplers.go +++ b/ring/ringqp/samplers.go @@ -11,6 +11,7 @@ type UniformSampler struct { } // NewUniformSampler instantiates a new UniformSampler from a given PRNG. +// WARNING: If the PRNG is deterministic/keyed (of type [sampling.KeyedPRNG]), *concurrent* calls to the sampler will not necessarily result in a deterministic output. func NewUniformSampler(prng sampling.PRNG, r Ring) (s UniformSampler) { if r.RingQ != nil { s.samplerQ = ring.NewUniformSampler(prng, r.RingQ) @@ -67,11 +68,3 @@ func (s UniformSampler) ReadNew() (p Poly) { return Poly{Q: Q, P: P} } - -func (s UniformSampler) WithPRNG(prng sampling.PRNG) UniformSampler { - sp := UniformSampler{samplerQ: s.samplerQ.WithPRNG(prng)} - if s.samplerP != nil { - sp.samplerP = s.samplerP.WithPRNG(prng) - } - return sp -} diff --git a/ring/sampler.go b/ring/sampler.go index ee6c3b7d..62d00912 100644 --- a/ring/sampler.go +++ b/ring/sampler.go @@ -61,6 +61,8 @@ type Ternary struct { // i.e., with coefficients uniformly distributed in the given ring. type Uniform struct{} +// NewSampler returns a new sampler that follows the distribution given by DistributionParameters. +// WARNING: If the PRNG is deterministic/keyed (of type [sampling.KeyedPRNG]), *concurrent* calls to the sampler will not necessarily result in a deterministic output. func NewSampler(prng sampling.PRNG, baseRing *Ring, X DistributionParameters, montgomery bool) (Sampler, error) { switch X := X.(type) { case DiscreteGaussian: diff --git a/ring/sampler_gaussian.go b/ring/sampler_gaussian.go index e721ec62..b154b2ed 100644 --- a/ring/sampler_gaussian.go +++ b/ring/sampler_gaussian.go @@ -23,6 +23,7 @@ type GaussianSampler struct { // NewGaussianSampler creates a new instance of GaussianSampler from a PRNG, a ring definition and the truncated // Gaussian distribution parameters. Sigma is the desired standard deviation and bound is the maximum coefficient norm in absolute // value. +// WARNING: If the PRNG is deterministic/keyed (of type [sampling.KeyedPRNG]), *concurrent* calls to the sampler will not necessarily result in a deterministic output. func NewGaussianSampler(prng sampling.PRNG, baseRing *Ring, X DiscreteGaussian, montgomery bool) (g *GaussianSampler) { g = new(GaussianSampler) g.baseSampler = &baseSampler{} diff --git a/ring/sampler_uniform.go b/ring/sampler_uniform.go index b4df1523..9e665a2d 100644 --- a/ring/sampler_uniform.go +++ b/ring/sampler_uniform.go @@ -12,6 +12,7 @@ type UniformSampler struct { } // NewUniformSampler creates a new instance of UniformSampler from a PRNG and ring definition. +// WARNING: If the PRNG is deterministic/keyed (of type [sampling.KeyedPRNG]), *concurrent* calls to the sampler will not necessarily result in a deterministic output. func NewUniformSampler(prng sampling.PRNG, baseRing *Ring) (u *UniformSampler) { u = new(UniformSampler) u.baseSampler = &baseSampler{} @@ -105,15 +106,6 @@ func (u *UniformSampler) ReadNew() (pol Poly) { return } -func (u *UniformSampler) WithPRNG(prng sampling.PRNG) *UniformSampler { - return &UniformSampler{ - baseSampler: &baseSampler{ - prng: prng, - baseRing: u.baseRing, - }, - } -} - // RandUniform samples a uniform randomInt variable in the range [0, mask] until randomInt is in the range [0, v-1]. // mask needs to be of the form 2^n -1. func RandUniform(prng sampling.PRNG, v uint64, mask uint64) (randomInt uint64) { diff --git a/utils/sampling/prng.go b/utils/sampling/prng.go index 435077f7..800cbb18 100644 --- a/utils/sampling/prng.go +++ b/utils/sampling/prng.go @@ -2,41 +2,46 @@ package sampling import ( "crypto/rand" - "fmt" "io" + "sync" "golang.org/x/crypto/blake2b" ) -// PRNG is an interface for secure (keyed) deterministic generation of random bytes +// PRNG is an interface for secure generation of random bytes type PRNG interface { io.Reader } -// KeyedPRNG is a structure storing the parameters used to securely and deterministically generate shared -// sequences of random bytes among different parties using the hash function blake2b. Backward sequence -// security (given the digest i, compute the digest i-1) is ensured by default, however forward sequence -// security (given the digest i, compute the digest i+1) is only ensured if the KeyedPRNG is keyed. -type KeyedPRNG struct { - key []byte - xof blake2b.XOF -} - type ThreadSafePRNG struct { } +// NewPRNG returns a new PRNG that is thread-safe +func NewPRNG() (*ThreadSafePRNG, error) { + return &ThreadSafePRNG{}, nil +} + // Read reads bytes from the KeyedPRNG on sum. func (prng *ThreadSafePRNG) Read(sum []byte) (n int, err error) { - tmpPRNG, err := NewPRNG() - if err != nil { - return 0, fmt.Errorf("crypto rand error: %w", err) - } - return tmpPRNG.Read(sum) + return rand.Read(sum) +} + +// KeyedPRNG is a structure storing the parameters used to securely and *deterministically* generate shared +// sequences of random bytes among different parties using the hash function blake2b. Backward sequence +// security (given the digest i, compute the digest i-1) is ensured by default, however forward sequence +// security (given the digest i, compute the digest i+1) is only ensured if the KeyedPRNG is keyed. +// WARNING: KeyedPRNG should NOT be called by multiple threads. It does not make sense to do so as the resulting +// sequence will not be deterministic for a given key. For a PRNG securely seeded with a private keyuse [ThreadSafePRNG]. +type KeyedPRNG struct { + mutex sync.Mutex + key []byte + xof blake2b.XOF } // NewKeyedPRNG creates a new instance of KeyedPRNG. // Accepts an optional key, else set key=nil which is treated as key=[]byte{} // WARNING: A PRNG INITIALISED WITH key=nil IS INSECURE! +// WARNING: KeyedPRNG should NOT be called by multiple threads. If that occurs, the generated sequence will not be deterministic. func NewKeyedPRNG(key []byte) (*KeyedPRNG, error) { var err error prng := new(KeyedPRNG) @@ -44,19 +49,6 @@ func NewKeyedPRNG(key []byte) (*KeyedPRNG, error) { return prng, err } -// NewPRNG creates KeyedPRNG keyed from rand.Read for instances were no key should be provided by the user -func NewPRNG() (*KeyedPRNG, error) { - var err error - prng := new(KeyedPRNG) - key := make([]byte, 64) - if _, err := rand.Read(key); err != nil { - return nil, fmt.Errorf("crypto rand error: %w", err) - } - prng.key = key - prng.xof, err = blake2b.NewXOF(blake2b.OutputLengthUnknown, key) - return prng, err -} - // Key returns a copy of the key used to seed the PRNG. // This value can be used with `NewKeyedPRNG` to instantiate // a new PRNG that will produce the same stream of bytes. @@ -67,11 +59,17 @@ func (prng *KeyedPRNG) Key() (key []byte) { } // Read reads bytes from the KeyedPRNG on sum. +// WARNING: Read() should NOT be called concurrently by multiple threads. If that occurs, the generated sequence will not be deterministic. func (prng *KeyedPRNG) Read(sum []byte) (n int, err error) { + prng.mutex.Lock() + defer prng.mutex.Unlock() return prng.xof.Read(sum) } // Reset resets the PRNG to its initial state. +// WARNING: KeyedPRNG's methods should not be called concurrently. If that occurs, the generated sequence will not be deterministic. func (prng *KeyedPRNG) Reset() { + prng.mutex.Lock() + defer prng.mutex.Unlock() prng.xof.Reset() }