Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Outline of a PRSS framework #40

Merged
merged 5 commits into from
Jul 12, 2022
Merged

Outline of a PRSS framework #40

merged 5 commits into from
Jul 12, 2022

Conversation

martinthomson
Copy link
Member

This uses x25519 to exchange secrets, HKDF (SHA-256) to derive secrets,
and AES as the PRF. I've only implemented the ability to generate a
u128 with random access and bits with sequential access so far.

The testing for the endpoint part is incomplete too.

@benjaminsavage let me know if this (or something like it) is in a usable shape.

We need to work out the field math still, but once I've done a bit more testing on this then I'll start to build out some basic fields.

# rust-elgamal (via curve25519-dalek-ng) only works with digest 0.9, not 0.10
digest = "0.9"
hex = {version = "0.4", optional = true}
# rust-elgamal (via curve25519-dalek-ng) only works with digest 0.9, so pin this
hkdf = "0.11"
log = "0.4"
# This is stupid, but we have packages that want this old interface
# those have to use the same RNG as packages that want the new interface.
old_rand_core = { package = "rand_core", version = "0.5", default-features = false }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be easier than what I did, yeah.

}

impl Generator {
/// Generate the value at the given index.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the full context here, how the values of index are picked by the clients of this API? do they set an initial value and then just monotonically increase it or pick at random using a PRNG?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drawing at random is generally a poor choice. I was thinking that the number would be structured. You have some number of bits dedicated to a record counter, some number of bits dedicated to a step number, or something like that.

There might need to be a layer on top that makes this part a bit easier to manage, I don't know.

// need lots of fields with different primes.

#[derive(Clone, Copy, PartialEq)]
pub struct Fp31(<Self as Field>::Integer);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow i haven't seen this syntax before, at least not as a field of a struct. im not even sure how this works

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic. I was a little surprised that this sort of recursiveness was tolerated by the compiler too. The syntax isn't new though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what this means =). In human language what is going on here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically Self is Fp31 when you are defining anything related to Fp31. But that is a nebulous type defined by multiple implementations. <Fp31 as Field> is specifically the implementation of Field for Fp31. That has an associated type of Integer, which turns out to be usable here.

Honestly, this was even more DRY than I would have thought possible, but given that rust accepted it, I left it there for us all to wonder at.

/// is true provided that `PRIME` is kept to at most 64 bits in value.
///
/// This method is simpler than rejection sampling for these small prime fields.
impl<T: Into<u128>> From<T> for Fp31 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so T: Into<u128> is sufficient for trait requirement From<u128>?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also magic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, when you define something as generic, what it really does is create what rust calls "monomorphisms" of the type for each of the concrete types that use the code. So as u128 implements Into<u128> (there is a default no-op implementation for that), the trait requirement is naturally fulfilled.

src/prss.rs Outdated
impl GeneratorFactory {
/// Create a new generator using the provided context string.
/// # Panics
/// Never: we don't let that happen.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it never panics, do you need to mention it? is it because clippy caught the .unwrap()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because clippy found the unwrap without understanding the conditions.

use rand::{CryptoRng, RngCore};
use sha2::Sha256;
use x25519_dalek::{EphemeralSecret, PublicKey};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any paper or write-up you can link to describing the goal of the math being done here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should do that write-up. Let me get back to you on that though.

Copy link
Collaborator

@benjaminsavage benjaminsavage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great. Definitely usable. I'd love to get this landed so that I can build on top of it!

// need lots of fields with different primes.

#[derive(Clone, Copy, PartialEq)]
pub struct Fp31(<Self as Field>::Integer);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what this means =). In human language what is going on here?

Comment on lines +45 to +50
impl AddAssign for Fp31 {
#[allow(clippy::assign_op_pattern)]
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code seems like it would be the same for all prime fields. Is there some way to put this into "Field" itself, and not Fp31?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is step 1. As you can see, it's likely to be very repetitive for new primes. The next step is to define a macro that includes all these implementations. There are a few choices we need to make (prime, basic type, upconversion type for addition and multiplication), but it should be a fairly easy thing to do.

src/field.rs Outdated
type Output = Self;

fn neg(self) -> Self::Output {
Self(Self::PRIME - self.0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is wrong. The negation of 0 is not PRIME.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, I forgot a modulo.

fn sub(self, rhs: Self) -> Self::Output {
// TODO(mt) - constant time?
// Note: no upcast needed here because `2*p < u8::MAX`.
Self((Self::PRIME + self.0 - rhs.0) % Self::PRIME)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be more complex for other primes =).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the generic implementation will need to upconvert, which - for types that don't need that upconversion, that is most of them - will need some nice warning suppression annotations.

Comment on lines +70 to +75
impl SubAssign for Fp31 {
#[allow(clippy::assign_op_pattern)]
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as the one on AddAssign. I'd love to only have this defined once - and not repeat it on Fp2147483647 and friends =).


assert_eq!(Fp31(16), x + y);
assert_eq!(Fp31(25), x * y);
assert_eq!(Fp31(1), x - y);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest adding a few more test cases like 0 + 0 and 0 - 0 and 1 + 0, 0 - 1, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the zero tests are fun. Easy to get that wrong.

(F::from(l), F::from(r))
}

/// Generate an additive share of zero.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is subtle. probably merits a comment explaining how it works. Took me a second to work it out =).

src/prss.rs Outdated
l - r
}

/// Generate additive shares of zero in a field.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy paste error for this comment I think


/// Generate the next share in `ZZ_2`
#[must_use]
pub fn next_zero_bit_share(&mut self) -> bool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might we also want a bitwise share which isn't always zero?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want random bit values, I think that it is enough to just independently draw at random. There is no need for coordination there.

src/prss.rs Outdated
Comment on lines 188 to 201
/// The current index, shifted left by 7 bits, plus a 7 bit index.
i: u128,
/// The value we got from the current index (this is outdated if `i & 0x7f == 0`).
v: u128,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get what is going on with the index =(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try again...

Copy link
Member Author

@martinthomson martinthomson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thurstonsand asked for a write-up. Here's a short one.

In summary, the process establishes pairwise shared secrets between all participants. It then uses a KDF to extract entropy from that secret and produce uniformly random, but distinct outputs for different uses. From those outputs it seeds a PRF.

The idea is that you want to generate a shared secret between each pair of participants. For that, I'm using X25519. Each participant generates a key pair and publishes its public key. I'm having each participant publish two keys, one for the participant to the left, one for the participant to the right.

From those 6 key pairs, we match up each and produce 3 shared secrets.

The output of this is not uniformly random (in X25519, the resulting value is from the large subgroup in the field). So we extract entropy from that using the extract function from HKDF. This produces a pseudorandom key.

We ask HKDF expand to produce enough bits for an AES key. I'm using AES-256 here because I can. Different HKDF labels allow us to create multiple unrelated shared secrets. This is a well-established technique (TLS uses it for example).

I'm using AES here as a raw PRF. Given an input (which I'm taking as u128), and the generated key, AES provides a pseudorandom permutation of that input. AES is used to output a u128 value that is effectively random.

A PRP is a PRF with a weird additional property that each input has a distinct output. That's probably not ideal, but this code destroys that property by taking the AES output modulo the field prime.

For more on taking a modulus...

Note that modulo is a terrible idea if the size of your field is close to the size of the random input. For example, if you had random 7 bit value and you are taking a modulus into GF(127), the value 0 has twice the probability of being chosen than any other value. Here we're going to be choosing fields that are at most 64 bits wide, so the bias is minuscule. (Rejection sampling might be possible, but then we'd need to reserve some number of bits from the input space for getting extra values.) Biases like this can be exploited in some settings (this is why RC4 is no longer considered good), but that sort of attack is not applicable in this setting...to my knowledge.

Adding extra bits to the random value dilutes the bias, effectively by over-sampling. An 8-bit value into GF(127) biases toward 0 and 1 50% more than other values, but a 16-bit value has just a 0.19% bias on 4 values. For the example of Fp31, there are 31 values, of which 23 have 10976850545836724323181988441692831744 chances of being picked and the remaining 8 have 10976850545836724323181988441692831745 chances. That is biased, but at 9e-36% or 2-123 probability that bias is negligible. As long as we choose primes that are small (i.e., less than 264) that amount of bias should not provide any advantage to an adversary.

The way I anticipate this being used is that the functioning of each helper is synchronized. Inputs are provided in the same order to each helper, allowing us to allocate a number to each. Each call that is made of these functions can thereby be given a unique number that all helpers can agree on. That input might be derived by allocating numbers to each record, to each step, and to each invocation of these functions. That allocation can use some convention we invent. The numbers we choose can be packed into a u128 (again using an agreed convention) and then used as an index to these functions.

// need lots of fields with different primes.

#[derive(Clone, Copy, PartialEq)]
pub struct Fp31(<Self as Field>::Integer);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically Self is Fp31 when you are defining anything related to Fp31. But that is a nebulous type defined by multiple implementations. <Fp31 as Field> is specifically the implementation of Field for Fp31. That has an associated type of Integer, which turns out to be usable here.

Honestly, this was even more DRY than I would have thought possible, but given that rust accepted it, I left it there for us all to wonder at.

Comment on lines +45 to +50
impl AddAssign for Fp31 {
#[allow(clippy::assign_op_pattern)]
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is step 1. As you can see, it's likely to be very repetitive for new primes. The next step is to define a macro that includes all these implementations. There are a few choices we need to make (prime, basic type, upconversion type for addition and multiplication), but it should be a fairly easy thing to do.

src/field.rs Outdated
type Output = Self;

fn neg(self) -> Self::Output {
Self(Self::PRIME - self.0)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, I forgot a modulo.

fn sub(self, rhs: Self) -> Self::Output {
// TODO(mt) - constant time?
// Note: no upcast needed here because `2*p < u8::MAX`.
Self((Self::PRIME + self.0 - rhs.0) % Self::PRIME)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the generic implementation will need to upconvert, which - for types that don't need that upconversion, that is most of them - will need some nice warning suppression annotations.

// TODO(mt) - constant time?
let c = u16::from;
#[allow(clippy::cast_possible_truncation)]
Self(((c(self.0) * c(rhs.0)) % c(Self::PRIME)) as <Self as Field>::Integer)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that multiplying two values from [0..31) produces a value that is > 8 bits. So I need to upconvert to u16. The cast takes it back down from u16 to u8.

I could use u8::try_from(...).unwrap() but that comes with a runtime cost.


assert_eq!(Fp31(16), x + y);
assert_eq!(Fp31(25), x * y);
assert_eq!(Fp31(1), x - y);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the zero tests are fun. Easy to get that wrong.

/// is true provided that `PRIME` is kept to at most 64 bits in value.
///
/// This method is simpler than rejection sampling for these small prime fields.
impl<T: Into<u128>> From<T> for Fp31 {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, when you define something as generic, what it really does is create what rust calls "monomorphisms" of the type for each of the concrete types that use the code. So as u128 implements Into<u128> (there is a default no-op implementation for that), the trait requirement is naturally fulfilled.


/// Generate the next share in `ZZ_2`
#[must_use]
pub fn next_zero_bit_share(&mut self) -> bool {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want random bit values, I think that it is enough to just independently draw at random. There is no need for coordination there.

src/prss.rs Outdated
Comment on lines 188 to 201
/// The current index, shifted left by 7 bits, plus a 7 bit index.
i: u128,
/// The value we got from the current index (this is outdated if `i & 0x7f == 0`).
v: u128,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try again...

This uses x25519 to exchange secrets, HKDF (SHA-256) to derive secrets,
and AES as the PRF.  I've only implemented the ability to generate a
u128 with random access and bits with sequential access so far.

The testing for the endpoint part is incomplete too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants