refactor: make keyedprng thread-safe, restrict usage + add warning

This commit is contained in:
lehugueni
2025-02-20 09:48:17 +01:00
parent b050a1caa0
commit 2da1be7e15
8 changed files with 42 additions and 55 deletions

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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{}

View File

@@ -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) {

View File

@@ -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()
}