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

Generic EC #186

Open
wants to merge 68 commits into
base: master
Choose a base branch
from
Open

Generic EC #186

wants to merge 68 commits into from

Conversation

dvdplm
Copy link
Contributor

@dvdplm dvdplm commented Feb 3, 2025

Make synderion generic over the elliptic curve type.

Very noisy PR, but the gist of it is that we make the SchemeParams trait agnostic over the elliptic curve used and thread that change across the code base while removing all explicit dependencies on k256.
Introduces a new generic parameter P: SchemeParams in many places, hence most of the noise.

The main areas to consider during review are:

  • Deserialization bounds. I have to admit that I often have very little clue as to why certain #[serde(bound(deserialize(STUFF))] are necessary or even why they work beyond "serde magic". Not great.
  • bip32 related functionality. BIP32 is only really well-defined for secp256k1 and while the code in this PR works for other curves as well, nothing but secp256k1 should ever be used in production without careful analysis.
  • The added HashOutput associated type on SchemeParams should be considered temporary. The goal is to remove the need for it and then we can remove it entirely.
  • The added WideCurveUint associated type on SchemeParams is there to allow reduced bias when generating scalars from extendable hashes. It's not great to have to do this, but I have found no better way. :/
  • There is an unfortunate delay in releasing updates to the RustCrypto stack following the release of crypto-bigint 0.6, which means that in this PR we're juggling two different versions of the bigint library. Hopefully we can get rid of this Real Soon™.

One outstanding question not addressed here is how to tweak the TestParams for optimal speed. For TinyCurve64 these settings seem to work:

-    const L_BOUND: u32 = 256;
-    const LP_BOUND: u32 = 256;
-    const EPS_BOUND: u32 = 320;
+    const L_BOUND: u32 = 64;
+    const LP_BOUND: u32 = 320;
+    const EPS_BOUND: u32 = 128;

…along with a PRIME_BITS setting of 256.

Closes #27.

@@ -5,13 +5,15 @@ use manul::{
dev::{run_sync, BinaryFormat, TestSessionParams, TestSigner, TestVerifier},
signature::Keypair,
};
use rand_core::OsRng;
use synedrion::{AuxGen, AuxInfo, InteractiveSigning, KeyInit, KeyShare, TestParams};
use primeorder::FieldBytes;
Copy link
Member

@fjarri fjarri Feb 21, 2025

Choose a reason for hiding this comment

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

We only need primeorder for this type, and it's just a type alias, perhaps we can just define it ourselves.

pub(crate) const ORDER: U256 = Secp256k1::ORDER;

impl HashableType for Curve {
impl HashableType for k256::Secp256k1 {
Copy link
Member

Choose a reason for hiding this comment

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

Can it be defined generically for any Curve implementor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think problem here was that there's a blanket impl that conflicts? But maybe I'm wrong. I'll try!

let bytes_lo = GenericArray::<_, FieldBytesSize<P::Curve>>::from_slice(&bytes_lo);
let bytes_hi = reader.read_boxed(Self::repr_len());
let bytes_hi = GenericArray::<_, FieldBytesSize<P::Curve>>::from_slice(&bytes_hi);
let uint_lo = <P::Curve as Curve>::Uint::decode_field_bytes(bytes_lo);
Copy link
Member

@fjarri fjarri Feb 21, 2025

Choose a reason for hiding this comment

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

Does that work with non-reduced data? Feels like this should be crypto_bigint::Encoding:from_be_bytes() and not something specific to ECC

let uint_lo = <P::Curve as Curve>::Uint::decode_field_bytes(bytes_lo);
let uint_hi = <P::Curve as Curve>::Uint::decode_field_bytes(bytes_hi);
let wide_uint = uint_lo.concat(&uint_hi);
// TODO: When the elliptic curve stack upgrades to crypto-bigint v0.6 we can use RemMixed and
Copy link
Member

Choose a reason for hiding this comment

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

You can use crypto-bigint 0.6 (the one we use for Paillier, for example) all the way and in the last line convert it to the old Uint, like in params::convert_uint()

Copy link
Member

Choose a reason for hiding this comment

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

Hm, but then you'll probably need to convert ORDER too...

pub fn from_reduced_bytes(bytes: &[u8; 32]) -> Self {
let arr = GenericArray::<u8, FieldBytesSize<Secp256k1>>::from(*bytes);
Self(<BackendScalar as Reduce<U256>>::reduce_bytes(&arr))
pub fn from_reduced_bytes(bytes: impl AsRef<[u8]>) -> Self {
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if this should reduce from a double-size uint as well.

@@ -25,7 +26,10 @@ use crate::{
/// - Safe serialization/deserialization (down to `serde` API; what happens there we cannot control)
pub(crate) struct Secret<T: Zeroize>(SecretBox<T>);

impl<T: Zeroize> Secret<T> {
impl<T> Secret<T>
Copy link
Member

Choose a reason for hiding this comment

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

Btw, do you use some tool to move the type bounds into the where clause? I would like to make it consistent throughout the codebase, but I couldn't find a lint in clippy or rustfmt setting that would do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, but it only works as a one-off thing, mapped to a key combo I have to press for each spot in the code. Having a way to do it across the code base would be real nice. Like, when I write code it's often convenient to type the bounds directly, but when reading code it's much easier (for me) to scan for the where clause; so automation would work well there for me.

@@ -83,31 +99,42 @@ impl Polynomial {
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct PublicPolynomial(Vec<Point>);
#[serde(bound(deserialize = "for<'x> P: Deserialize<'x>"))]
Copy link
Member

Choose a reason for hiding this comment

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

Strange that you need an explicit bound for Deserialize but not for Serialize

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. See other comment.

@@ -175,7 +175,6 @@ impl HasWide for U4096 {
type Wide = U8192;
}

// TODO(dp): Suggest crypto-bigint update nlimbs! macro.
Copy link
Member

Choose a reason for hiding this comment

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

We can probably bring back nlimbs! usage, it doesn't produce clippy warnings anymore.

for tweak in tweaks {
public_key = public_key.derive_child(*tweak)?;
}
let offset = bip32::KEY_SIZE - <C as Curve>::FieldBytesSize::USIZE;
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I understand how this works. bip32::KEY_SIZE = 32, <C as Curve>::FieldBytesSize::USIZE is 32 for k256 or 24 for tiny-curve. Then the length of public_key.to_bytes() is 33, with all of these bytes being meaningful for k256. Then we just drop the first byte (the sign of y) and it works as a SEC1 representation?

Copy link
Member

Choose a reason for hiding this comment

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

Also the question above about unwrapping applies here as well - I don't think you need to de/serialize at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me spell out my reasoning and you can shoot it down!

  • For k256 the offset is 0, so bytes.get(offset..) will be the whole slice of 33 bytes yes? Should behave the same as the old code.
  • For TinyCurve64 the offset is 8, so in this case we truncate the high 8 bytes. This is likely not ok for a proper SEC1 repr, but the premise here is that BIP32 isn't really defined for anything other than secp256k1 so it's unclear what "valid" would even mean.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, perhaps it does work, but in any case it's not necessary - we can avoid serialization entirely, see the other comment.

tweakable_sk = tweakable_sk.derive_child(*tweak)?;
}
let bytes = tweakable_sk.to_bytes();
let bytes = bytes.get(32 - Scalar::<P>::repr_len()..).ok_or(bip32::Error::Decode)?;
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't it be easier to just unwrap SecretTweakable into SecretKey, via another method in the SecretTweakable trait?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you elaborate a bit on this (and "I don't think you need to de/serialize at all" above), not sure how you mean!

Copy link
Member

Choose a reason for hiding this comment

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

See fjarri@fa15322 (naming is rough, bounds may be adjusted, and perhaps some arguments can be taken by value). Basically, you have a trait to convert VerifyingKey<C> to something bip32 accepts, so I just add another method to convert that something back to VerifyingKey<C>, which we can do either by just returning the same object (for k256) or unwrapping the newtype (for tiny-curve). Same for SigningKey.

pub trait SchemeParams:
'static + Debug + Clone + Send + PartialEq + Eq + Send + Sync + Ord + Copy + Serialize + for<'x> Deserialize<'x>
where
<Self::Curve as CurveArithmetic>::ProjectivePoint: FromEncodedPoint<Self::Curve>,
Copy link
Member

Choose a reason for hiding this comment

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

Some of the bounds look like they could be removed. The Copy ones, for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I assume you mean Copy on SchemeParams? Yeah I was on the fence on that, and tried both with and without; in the end I think that the reduced noise all over the code base from having it be Copy is worth it.

Copy link
Member

Choose a reason for hiding this comment

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

No, the Copy ones in the where clause. The Copy on SchemeParams is reasonable, I think clippy will complain if it's not present.

}

/// ZK proof: Paillier Affine Operation with Group Commitment in Range.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound(deserialize = "for<'x> P: Deserialize<'x>"))]
Copy link
Member

Choose a reason for hiding this comment

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

It's kind of weird to see these bounds everywhere. I thought about dealing with #10 by bounding SchemeParams on Serialize/Deserialize, which you did, but I would expect that to eliminate the need for the explicit bounds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This stuff was driving me nuts. I cannot tell you why this works or if there are other/better ways of doing it. It was by far the most frustrating thing about this work. You add a bound in one place and it typechecks, but then breaks somewhere else, you fix it over there in a way that makes sense and now it breaks somewhere else...

Copy link
Member

Choose a reason for hiding this comment

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

Filed serde-rs/serde#2899. These bounds can be avoided if we don't bound SchemeParams on Deserialize, but then other explicit bounds will be needed - but we already had those, so maybe that's the best course of action.

fn derive_verifying_key_bip32(&self, derivation_path: &DerivationPath) -> Result<VerifyingKey<C>, bip32::Error>;
}

mod sealed {
Copy link
Member

Choose a reason for hiding this comment

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

What's up with sealing? Do these traits show up in the docs otherwise?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea was to lock down the types for which it's ok to impl them, not really about hiding from docs. You think it's overkill? They have to be pub because the types they are impl'd for are pub.

@fjarri
Copy link
Member

fjarri commented Feb 21, 2025

One outstanding question not addressed here is how to tweak the TestParams for optimal speed. For TinyCurve64 these settings seem to work:

Yes, that looks correct, I added a comment in params.rs before reading the PR description :)
You can check the parameter validity by adding the test for are_self_consistent(), for them.

Also, there are three remaining comments mentioning #27 (in curve.rs, sigma/fac.rs, and paillier/encryption.rs) which can be now removed/amended.

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.

Generalize the library to use any curve
2 participants