-
Notifications
You must be signed in to change notification settings - Fork 53
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
Add frost-secp256k1-tr crate (BIP340/BIP341) [moved] #730
base: main
Are you sure you want to change the base?
Conversation
|
This PR clearly needs to be resynced with |
As noted by @jesseposner during the round table, the raw DKG output is not safe to use in a BIP341 context. I'm going to update this PR to reflect that, which might entail some API changes. |
I had the impression that this PR would adjust the sign of the X coordinate during signing? That would solve the issue. (I ask this because I remember the first thing I wanted to do in this PR was to make that during key generation only, but that's not possible by @jesseposner's point) |
There's a negation algorithm in the FROST signing BIP: https://github.com/siv2r/bip-frost-signing It gets a little tricky to account for both BIP32 tweaks and taproot script tweaks (TapTweaks), which is why the algorithm is a bit complex. The algorithm is implemented here: BlockstreamResearch/secp256k1-zkp#278 It's also the same algorithm used in the MuSig2 BIP and implementation. |
@conradoplg Yes, this PR does handle key negation properly at signing time (i haven't read Jesse's linked negation algorithm yet), but how would key negation at signing time prevent a cosigner from injecting a malicious tweak into the group key at DKG time? Maybe it'd help to actually write out the steps the attacker will do, so we're sure we're all talking about the same thing. Let's say we run a DKG to construct a simple 2-of-2 FROST group key. Honest participant A malicious participant
The resulting group pubkey accepted by the group will be: The DKG will succeed because the malicious participant does actually know the coefficients of their secret contribution polynomial and can produce a valid proof of possession. The only drawback of this attack is that it depends on the attacker waiting to hear everyone else's DKG commitments first, in order to compute the rogue tweak. And naturally it doesn't work if the group intends to use BIP32 or BIP341 tweaks after DKG, but if the group uses the raw DKG output pubkey |
@conduition Perhaps a simple solution is to tweak the DKG output in a way similar to BIP32? It could be as simple as tweaking by the group public key hash? Then the private shares, public shares and group key provided by the library can include this tweak with no API changes needed. |
@MatthewLM Great idea. It's simple and elegantly prevents the footgun, without limiting how participants use their group key. The group can still add further tweaks (e.g. BIP341, BIP32, etc) at signing time too. I spent last night trying to break it, but couldn't see anything wrong. @jesseposner @conradoplg What do you guys think? |
I'm starting the process of resyncing this PR with the upstream v2.0.0 changes on |
This sounds like what @real-or-random suggested here: BlockstreamResearch/bip-frost-dkg#41 (comment)
|
BIP341 suggests this tweak specifically: |
Isn't the key already tweaked at sign-time anyway? Looking at my own code, I've used an earlier version of this branch and I see that a TapTweak is already done. I was previously able to create successful Taproot signatures with key-spend tweaks. I'm not sure any changes are needed. I guess the fear is that the public key might be used outside the library without BIP341 compliance? In that case, pre-tweaking the DKG output might increase safety. |
Resyncing is done but not yet pushed to this branch. You can preview the changes here: main...conduition:frost:add-secp256k1-tr-reloaded @conradoplg Do you have any preference for how i merge the updates into this branch? If it's not important, i'll just force-push the new commit history over this PR. I tagged @mimoo and zebra-lucky as co-contributors in the commit messages but i'm not sure how else to properly attribute.
@MatthewLM Not always. The current code will, by default, sign without any tweak. The untweaked group pubkey which those signatures would verify under is susceptible to rogue tweak attacks at DKG-time. I think a static unspendable tweak of the group key at DKG time is the ideal scenario here, as this ensures that no sane caller will ever accidentally pay money to a group pubkey which could contain a rogue tweak. I'll see about adding this soon after merging the re-synced branch into this one. I'll try to follow the suggestion of BIP341 (using |
Just to formalize this 'tweak the DKG output' idea a little... Every FROST participant receives three things from the DKG:
To prevent malicious tweaks (inserted into some
The participants can do this implicitly after DKG, without any extra communication rounds. The shares are still valid and the resulting group key |
It doesn't matter, PRs are squashed in this repo anyway. GitHub will include the contributors in the commit message when it merges. Let me know if you need help solving conflicts. |
This commit contains changes to the frost-core crate which allow ciphersuites to better customize how signatures are computed. This will enable taproot support without requiring major changes to existing frost ciphersuites. Co-authored by @zebra-lucky and @mimoo This work sponsored by dlcbtc.com and lightspark.com
Co-authored by @zebra-lucky and @mimoo This work sponsored by dlcbtc.com and lightspark.com
Co-authored by @zebra-lucky and @mimoo This work sponsored by dlcbtc.com and lightspark.com
5807cb4
to
11f43eb
Compare
I've resynchronized this branch with upstream |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #730 +/- ##
==========================================
+ Coverage 82.18% 82.73% +0.55%
==========================================
Files 31 37 +6
Lines 3188 3950 +762
==========================================
+ Hits 2620 3268 +648
- Misses 568 682 +114 ☔ View full report in Codecov by Sentry. |
Just FYI I'm actively reviewing this. It looks good, but I think it might be possible to simplify some of the code so I'm working on that. |
I pushed a cleanup of the code. My main motivation was to minimize changes to I change the approach of the additional One aspect which I'm not sure about is tweak handling. In my commit I reverted the This means that, with my changes, tweak handling is not longer "transparent". You will need to call e.g. There is also some stuff missing:
I apologize for changing everything so much, but I feel like this approach has some advantages and makes everything a bit easier to reason about. I really appreciate all the effort that went into this. You can look at the individual commit to see the changes from the original approach, and the overall PR diff to see differences with Looking forward to your feedback. |
Thanks @conradoplg, I'll try to take a look at these changes soon. |
Thinking about it I don't think we can get away with not using even-Y for DKG proof-of-knowledge since the signature serialization only encodes X and the PoK is represented as a |
Ah nevermind. I am using even-Y for the R component of the signature. It's just the DKG |
signing_target: SigningTarget<C>, | ||
) -> Result<(SigningTarget<C>, Signature<C>, VerifyingKey<C>), Error<C>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't finished reviewing yet but I'm leaving this note here to remind me to ensure we have tests in the taproot crate covering tweaked signing, since check_sign
now only does untweaked signing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a test in tweaking_tests.rs
, where I basically copied the README test and added tweaking. I don't like the repetition but I think it will do it for now.
!= (group_commitment_share.to_element() | ||
+ (verifying_share.to_element() * challenge.0 * lambda_i)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notes:
- The group commitment is negated in the ciphersuite
verify_share
method - The verifying share is negated in the
aggregate
function, via the ciphersuitepre_aggregate
method
frost-secp256k1-tr/src/lib.rs
Outdated
// Compute the group commitment, negating if required by BIP-340. Note that | ||
// only at this point it is possible to check if negation will be required | ||
// or not. If it is, it will require negating the participant's nonces (when | ||
// signing) or their group commitment share (when aggregating). This is | ||
// signaled by setting the `is_group_commitment_even` flag in the Context. | ||
fn compute_group_commitment( | ||
ctx: &mut Self::Context, | ||
signing_package: &SigningPackage, | ||
binding_factor_list: &frost_core::BindingFactorList<S>, | ||
) -> Result<GroupCommitment<S>, Error> { | ||
let group_commitment = compute_group_commitment(signing_package, binding_factor_list)?; | ||
ctx.is_group_commitment_even = | ||
(!group_commitment.clone().to_element().to_affine().y_is_odd()).into(); | ||
let group_commitment = if !ctx.is_group_commitment_even { | ||
GroupCommitment::<S>::from_element(-group_commitment.to_element()) | ||
} else { | ||
group_commitment | ||
}; | ||
|
||
Ok(group_commitment) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for the wall of text here, i wanted to be detailed so that future-me will understand what present-me is planning 😉
We might be able to prune this method, and the whole Ciphersuite::Context
related-type idea that you're using to cache is_group_commitment_even
.
<C>::compute_group_commitment
is only used in two locations:
-
frost-core/src/round2.rs
in thesign
function. -
frost-core/src/lib.rs
in theaggregate
function.
For (1), the resulting group commitment is passed to the <C>::challenge(...)
invocation, but in BIP340, the challenge only commits to the x-coordinate of the group's public nonce (AKA group_commitment.to_element().to_affine().x()
). So it doesn't matter if the group commitment point is negated here or not. The only place in which the commitment parity does matter is when calling <C>::compute_signature_share
, where we conditionally negate our signer's secret nonces.
For (2), the resulting group commitment is used to construct the aggregated Signature
. We verify the signature using <C>::verify_signature
, and if that fails we start hunting for cheaters. The only two spots in here where the parity of Signature.R
matters are:
- the call to
<C>::verify_signature
to check the aggregate signature. - the call to
<C>::verify_share
to detect cheaters.
What if we change the Secp256K1Sha256TR::pre_verify
implementation to convert signature.R
into an even-parity point before verification? This would mean verifiers would accept a Signature
with an even OR an odd parity nonce, but that's OK because the parity bit will be discarded eventually anyway (BIP340 sigs dont have nonce parity). This means we don't need to negate the group commitment during aggregation.
Then we can get rid of the compute_group_commitment
method on ciphersuite, remove the Context
concept, and just pass in the un-negated group commitment into the ciphersuite methods which need to know <C>::compute_signature_share
and <C>::verify_share
).
If you like this idea i'd be happy to implement it, but before we do any more changes I'd like to get the test vectors updated and have tests passing first. I'll work on that over the next few days
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds great! I'd like to avoid having to pass the group commitment around if we could, but this feels much better than having to add Context
. Please go ahead if you want, thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@conradoplg I've pushed these changes, Context
is no more! 🚀
Uses r = e99ae2676eab512a3572c7b7655d633642a717250af57a7e0ccd5f9618b69f3f
@conradoplg Tests are passing for me on this branch 🎉 Now that we've got that sorted out, i'm going to do the pruning i suggested here to hopefully reduce code surface area. Fingers crossed it works. Might take a few days but i'll check in again soon 🙂 |
@conradoplg yes happy to run the testsuite again. I added a test to cover the taptweak case. In this test, we spend using the FROST key through the default key spend path, while also including optional tapleaf script paths. All tests are passing against the latest commit (f9237f5) |
With a couple of small adjustments to the code, we can remove the need for this extra associated type on the Ciphersuite crate. Accepting signature with odd-parity nonce values is OK, because BIP340 discard the nonce parity bit anyway.
This PR is a re-opening of #584 to resolve PR ownership problems. I'm opening this so zebra-lucky (who is short on time) doesn't need to be the middle-man merging my commits into their PR anymore.
This PR adds a new ciphersuite crate
frost-secp256k1-tr
, which complies with BIP340 Schnorr Signatures on Bitcoin and is compatible with BIP341 tweaks. This crate can be used to produce valid signatures on Bitcoin transactions.We handle the complex x-only negation conditions of BIP340 by introducing new methods on the
Ciphersuite
trait, which are generally identity functions in the case of other ciphersuites, but which in the taproot suite will conditionally negate certain values at critical times to maintain compatibility with BIP340's x-only public keys. These methods are usually denotedC::effective_xyz(...)
.To support BIP341 taptree tweaking, we add two new types,
SigningTarget
andSigningParameters
, and modify the signature package to accept any type that transformsInto
SigningTarget
. For backwards compatibility, this includes any type which implementsAsRef<[u8]>
. These convert intoSigningTarget
paired withSigningParameters::default()
. TheSigningParameters
can be modified to commit signatures to a specific tapscript merkle root tweak, allowing FROST groups to modify (tweak) their group keys with taproot script-spending paths, while retaining the use of the key-spending path.Some additional features we can consider adding:
i have a separate feature branch,
taproot-bip32
, based off of this branch which adds BIP32 support to thefrost-secp256k1-tr
crate. See this discussion for more background. It would be nice to be able to PR that branch into this repo as well, so that FROST keys can be used in hierarchical and descriptor wallets. For now I am leaving that separate to reduce conflation of features, but they can be merged together if desired.