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

Schnorr sign-to-contract and anti-exfil #1140

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

Conversation

benma
Copy link
Contributor

@benma benma commented Sep 11, 2022

This adds sign-to-contract and anti-exfil protocol functionality to the schnorrsig module. It is based on the same functions that already exist for ECDSA here:

https://github.com/ElementsProject/secp256k1-zkp/blob/d22774e248c703a191049b78f8d04f37d6fcfa05/include/secp256k1_ecdsa_s2c.h

Some commits and functions were adapted from the original work here: #589

Comment on lines 16 to 17
static const unsigned char s2c_data_tag[16] = "s2c/schnorr/data";
static const unsigned char s2c_point_tag[17] = "s2c/schnorr/point";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If these tags are okay, I can add functions with precomputed states if desired.

@real-or-random
Copy link
Contributor

Nice!

By the way, is there a reason why this uses S2C instead of simple addition (Scheme 6 in https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2020-March/017667.html) besides the fact that the ECDSA code uses S2C. Are you aware of any reason why the S2C variant would be preferable over Scheme 6? (I think @jonasnick once told me that he wasn't aware of all the various variants when the work was started.)


It seems the tests fails on s390x, which is a big-endian machine, see https://cirrus-ci.com/task/6416044628639744?logs=cat_tests_log#L4 Is there some code in this PR where endianness could be a problem? I don't see any but I only had a brief look.

@benma
Copy link
Contributor Author

benma commented Sep 13, 2022

By the way, is there a reason why this uses S2C instead of simple addition

I did it this way for two reasons:

About s390x: I am puzzled, I don't see anything related to endianness in the code related to the failing test, but maybe I am missing something. I also replaced the uint64_t magic by an 8-byte buffer just in case there could be problems with large integers, but it didn't seem to be the cause.

@real-or-random
Copy link
Contributor

About s390x: I am puzzled, I don't see anything related to endianness in the code related to the failing test, but maybe I am missing something. I also replaced the uint64_t magic by an 8-byte buffer just in case there could be problems with large integers, but it didn't seem to be the cause.

According to the implementation of counting_illegal_callback_fn excepts an int32_t and not size_t. Maybe this is the problem.

*/
typedef struct {
/* magic is set during initialization */
uint64_t magic;
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 took this for a commit from @jonasnick, but I am actually unsure what the purpose is of the magic. Absent programming errors inside the library, could something go wrong if there was no magic field? Are there guidelines of when to use a magic (most structs don't contain any)?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the magics are a relative recent addition, and just there for assurance, so that incorrect use can be detected. In correct usage, they serve no function.

@benma
Copy link
Contributor Author

benma commented Sep 13, 2022

About s390x: I am puzzled, I don't see anything related to endianness in the code related to the failing test, but maybe I am missing something. I also replaced the uint64_t magic by an 8-byte buffer just in case there could be problems with large integers, but it didn't seem to be the cause.

According to the implementation of counting_illegal_callback_fn excepts an int32_t and not size_t. Maybe this is the problem.

Great catch, that seemed to be the problem! Fixed now.

@benma benma marked this pull request as ready for review September 15, 2022 14:28
@benma
Copy link
Contributor Author

benma commented Sep 29, 2022

Most of the commits are taken as-is from @jonasnick and @apoelstra, with the added exfil functions working the same way as in the ECDSA version in secp256k1-zpk.

It would be great if you could review this and let me know if there is any obstacles to getting this merged. I'd like to use this in the BitBox02 to extend the anti-exfil protocol to Schnorr sigs in Taproot inputs.

@real-or-random
Copy link
Contributor

It would be great if you could review this and let me know if there is any obstacles to getting this merged

I just want to let you know that this on our radar. Things can move slow here and I think most of the contributors have been pretty busy in the remaining weeks (and at least I will still be busy with other stuff for ~2 weeks). I can't speak for the others but this is a feature I'd like to see included in the library, so I'll certainly come back to this soon.

@benma
Copy link
Contributor Author

benma commented Feb 14, 2023

It would be great if you could review this and let me know if there is any obstacles to getting this merged

I just want to let you know that this on our radar. Things can move slow here and I think most of the contributors have been pretty busy in the remaining weeks (and at least I will still be busy with other stuff for ~2 weeks). I can't speak for the others but this is a feature I'd like to see included in the library, so I'll certainly come back to this soon.

Gentle reminder 😄 The last time I did this for ECDSA, the frequent rebasings were sometimes actually very time-consuming and complicated. If would be great to review and merge this before complicated conflicts appear.

@benma
Copy link
Contributor Author

benma commented May 3, 2023

Rebased to fix conflicts with master.

@benma
Copy link
Contributor Author

benma commented Jun 19, 2023

@real-or-random @jonasnick @apoelstra any chance of progress here? The code should be straight forward, with a little bit of concentrated effort we could get this merged and used in production 🙏

*/
typedef struct {
/* magic is set during initialization */
uint64_t magic;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the magics are a relative recent addition, and just there for assurance, so that incorrect use can be detected. In correct usage, they serve no function.

*/
typedef struct {
unsigned char magic[4];
secp256k1_nonce_function_hardened noncefp;
void *ndata;
secp256k1_schnorrsig_s2c_opening* s2c_opening;
Copy link
Contributor

Choose a reason for hiding this comment

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

Adding fields to this struct is, I think, an incompatible API break. If a user of the library uses an old version of the header, and links against a new version of the library, I believe the library will access uninitialized fields. Is that right?

In theory, that's ok, because we're still in 0.x releases, so any breaks are allowed by SemVer, but this seems unnecessarily burdensome for users. Ideally, extra fields to the struct only affect calls that are impossible to make in old code (e.g. only by new API functions, or when function arguments are set to constants that don't exist in old versions).

Idly wondering: can we (ab)use the magic for this? Change the magic in the header, and make the library check whether the old or the new magic is used, and trigger behavior based on that.

Copy link
Contributor Author

@benma benma Jul 11, 2023

Choose a reason for hiding this comment

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

I believe the library will access uninitialized fields. Is that right?

That is right, good catch.

There could be multiple solutions:

  1. As you suggest, the magic can be used as a version field.
  2. The s2c_opening struct itself contains a magic field. Instead of calling secp256k1_schnorrsig_s2c_opening_init(s2c_opening) to initialize it inside the signing function, we could have the caller do this instead and check that the magic is set properly.
  3. not adding these fields to extraparams but add another function secp256k1_schneorrsig_sign_s2c instead.

It seems to me that option 2 is worse, as if the opening magic is not set properly (e.g. by a user using the old headers with the new library), then the function would error. This is better than proceeding silently, but it still is incompatible and forces the user act. The alternative to erroring is to ignore these fields to maintain compatibility, but that would be strange for new library users that want to use the functionality and don't see a proper error message when they fail to initialize the opening struct.

Option 3 seems backwards as the point of secp256k1_schnorrsig_extraparams struct was, I presume, to be able to extend it in a backwards compatible fashion.

Option 2 probably makes sense to do for the same reason that the extraparams struct is initialized by the user - to prevent corrupting memory in case the user passes the wrong pointer, and to possibly use it as a version field in the future.

TL;DR: I think defining a new magic in the extraparams struct is the best option, and we should also have the caller initialize the secp256k1_schnorrsig_s2c_opening struct with the magic. What do you think?

As a sidenote: maybe it would be a good to introduce a version check or init function for this library in general, so that if breaking changes are necessary in the future, it could be caught early at runtime.

Copy link
Contributor

Choose a reason for hiding this comment

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

The extraparams struct was built with the intention that magic acts as versioning for the struct and therefore allows backward-compatible changes (there's some mention of this in the original schnorrsig_sign_custom PR). I implemented a variant of the idea here. I was imagining to change schnorrsig_sign_custom to something like this:

int secp256k1_schnorrsig_sign_custom(..., secp256k1_schnorrsig_extraparams *params_in) {
    secp256k1_schnorrsig_extraparams params;

    if (memcmp(params_in->magic, old_magic, sizeof(params_in->magic)) == 0) {
        secp256k1_extraparams_old *params_tmp;
        copy_old_into_new(&params, params_tmp);
    } else if (memcmp(params_in->magic, SECP256K1_SCHNORRSIG_EXTRAPARAMS_MAGIC, sizeof(params_in->magic)) == 0) {
        params = *params_in;
    }
    /* ... */
}

While this does work in practice, I'm not sure how compliant this is with the C standard. It would access the old extraparams struct through a pointer to a new extraparams struct. Padding shouldn't be an issue here because it only accesses the first member of the struct.

It would be better if there was a solution where correctness is more obvious.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure how compliant this is with the C standard.

If you read the standard strictly, then it's just UB to have conflicting declarations for the same function: https://stackoverflow.com/a/69620952

In practice, and probably even with a more pragmatic reading of the standard, it's unclear what should go wrong. Following your example code, the callee will read the magic value by accessing bytes through an lvalue of type unsigned char. That's always allowed (even if the new struct type has stricter alignment requirements than the old struct type). Moreover, the magic is guaranteed to be at the beginning of the struct, and it's explicitly allowed to use pointers to a struct as pointers to the first element (after an appropriate cast). After determining the version, the callee will read the struct members through an lvalue of the correct type (not identical type with same name but "compatible type"), which is also allowed.

As the callee starts with reading characters, it's very much like passing an unsigned char* directly to the function and then casting it a pointer to the right struct type. I think we can do this in practice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the helpful inputs everyone!

I added a new commit use the magic in the schnorrsig extraparams struct for versioning that implements this for easier review. I can squash it in the end. Please take a look.

/** The signer commitment in the anti-exfil protocol is the original public nonce. */
typedef secp256k1_pubkey secp256k1_schnorrsig_anti_exfil_signer_commitment;

/** Parse a sign-to-contract opening.
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be possible to add a succinct description of the actual protocol to this header, or a reference to it?

The idea is that this is essentially defining a new cryptographic protocol that isn't formally specified anywhere. If so, it'd be good to have the specification right here, so someone doesn't need to go read through the implementation in order to figure out what the exact protocol is.

That makes it easier to separately review the protocol itself, and compare the implementation to that protocol.

As an example, see https://github.com/bitcoin-core/secp256k1/pull/1129/files#diff-d3089adfea8d8bbc31e130bb67389ab9ef45ea4abf4a79eceec63a037359f39dR10-R44 for example, which does so for ElligatorSwift.

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 added a specification in the header (in a new commit for easier review, will squash in the end).

The spec is mostly a copy/paste from

A very similar spec was present in @jonasnick's original PR:

) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);


/** Create the initial host commitment to `rho`. Part of the Anti-Exfil Protocol.
Copy link
Contributor

Choose a reason for hiding this comment

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

Here as well, I think it'd be good to add a specification of the Anti-Exfil protocol, and/or references to such a specification.

@benma
Copy link
Contributor Author

benma commented Aug 5, 2024

@jonasnick @sipa @real-or-random should I continue here or open an equivalent PR in secp256k1-zkp, or something else?

It seems close to being ready for production but got stuck due to uncertainty about whether this belongs in this repo in the first place.

@jonasnick
Copy link
Contributor

@benma I agree with sipa's comment above that a specification would be helpful. According to our CONTRIBUTING.md it is recommended to provide a specification and security arguments in order to add a new module. I think the relevance criterion for the new module is met.

@benma benma force-pushed the schnorr-s2c branch 2 times, most recently from 21c7cef to 3de6aed Compare October 11, 2024 00:36
jonasnick and others added 5 commits October 11, 2024 11:29
Adapted from bitcoin-core#589.

The data is hashed using a tagged hash with the "s2c/schnorr/data"
tag, which is consistent with the data hashing done in the ECDSA
version of sign-to-contract (in ElementsProject/secp256k1-zkp), where
the "s2c/ecdsa/data" tag is used. Similarly, the tweak hash tag is
"s2c/schnorr/point".

Co-authored-by: Jonas Nick <[email protected]>
These functions allow to perform the anti-exfil protocol. It is very
similar to the implementation of the same protocol for ECDSA in
ElementsProject/secp256k1-zkp.

The opening struct can't be use in
`secp256k1_schnorrsig_anti_exfil_signer_commit()` as it contains the
``nonce_is_negated` field, which can only be set correctly during
signing with s2c data. As a result, we must use the opening in the
commitment verification, so we also must check that the signer
commitment is the same as the one used during signing. The alternative
is to only compare the x-coordinate, in which case the opening struct
could skip `nonce_is_negated` and the struct could be reused in
`secp256k1_schnorrsig_anti_exfil_signer_commit()`, but it seems to
have a downside that it would prevent batch-verification of the
commitments.
This ensures compatibility in that it makes sure that the
`secp256k1_schnorrsig_sign_custom()` works for users using an older
version of the headers but linking against a newer version of the
library.
@benma
Copy link
Contributor Author

benma commented Oct 11, 2024

@benma I agree with sipa's comment above that a specification would be helpful. According to our CONTRIBUTING.md it is recommended to provide a specification and security arguments in order to add a new module. I think the relevance criterion for the new module is met.

Thanks. I added the specification in the comments, see #1140 (comment).

Please take another look @jonasnick @sipa @real-or-random.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants