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

redo fuzz target #264

Merged
merged 5 commits into from Dec 28, 2020
Merged

redo fuzz target #264

merged 5 commits into from Dec 28, 2020

Conversation

apoelstra
Copy link
Member

@apoelstra apoelstra commented Dec 22, 2020

The existing fuzz target code had a number of problems

  • It unnecessarily reimplemented parsing and serialization logic that we could just fuzz directly
  • It didn't implement any of the Schnorr stuff
  • It wasn't internally consistent, in the sense that all the unit tests would fail with it

This PR reimplements signing/verification for both Schnorr and ECDSA, such that a fuzzer should be able to forge "valid" signatures.

It does not implement ECDH, since I couldn't find a reasonable way to get a commutative function of (secret key, public key) that the fuzzer could plausibly fake, nor did I implement the tweak_add and tweak_add_check API for the same reason. I think the ECDH case is probably okay because you never "verify" an ECDH secret without recomputing it, so the fuzzer will know what value to target.

@apoelstra apoelstra force-pushed the 2020-12--no-extsymb2 branch 2 times, most recently from 68755f3 to 904ddad Compare December 22, 2020 22:04
@elichai
Copy link
Member

elichai commented Dec 23, 2020

One of the doc tests is failing with the fuzzing enabled

   Doc-tests secp256k1

running 5 tests
test src/lib.rs - Secp256k1<C>::verify (line 703) ... ok
test src/lib.rs - (line 39) ... ok
test src/ecdh.rs - ecdh::SharedSecret::new_with_hash (line 129) ... ok
test src/lib.rs - (line 75) ... FAILED
test src/lib.rs - (line 59) ... ok

failures:

---- src/lib.rs - (line 75) stdout ----
Test executable failed (exit code 101).

stderr:
thread 'main' panicked at 'assertion failed: secp.verify(&message, &sig, &public_key).is_ok()', src/lib.rs:34:1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

@elichai
Copy link
Member

elichai commented Dec 23, 2020

Also, just making sure I understand correctly, this only overrides sign/verify for fuzzing, it doesn't override context creation and relevant code, right?

@apoelstra
Copy link
Member Author

@elichai correct. To override context creation would require I override pubkey_create, which would require I come up with a whole new group law, which would require I override pubkey_parse and pubkey_serialize, and then at that point I've rewritten every function. Which is what the original code tried to do.

As for the failing doctest, there seems to be a Cargo bug where I can't propagate --cfg flags into the doc tests :( so I don't know how to fix this.

@apoelstra
Copy link
Member Author

Got it! There is a second variable RUSTDOCFLAGS I had to set.

You should be able to run cargo test --all-features now with the fuzztarget on.

@apoelstra
Copy link
Member Author

BTW, in getting this to work, I notice that we broke the old fuzz implemntation with #233. So this PR is release-blocking.

We can now run unit tests with the fuzz feature on, and they'll pass,
which is some assurance that fuzzing with the feature on won't lead to
spurious failures due to the fuzz harness inadequately simulating message
signing.
@TheBlueMatt
Copy link
Member

It does not implement ECDH, since I couldn't find a reasonable way to get a commutative function of (secret key, public key) that the fuzzer could plausibly fake, nor did I implement the tweak_add and tweak_add_check API for the same reason

I believe rust-lightning's fuzz stuff needs both? Maybe only the tweak stuff, though, I'd have to check.

@apoelstra
Copy link
Member Author

It would surprise me if you were using tweak_add in any way that the fuzzer would've had difficulty with, since the original code was not consistent (if you tweaked a secret key and public key with the same tweak, the resulting secret/public keys would not match).

One interesting thing we could try would be to compile libsecp with an order-13 group (rather than the ~2^256-order group that we normally use). And then not mock any functions.

@TheBlueMatt
Copy link
Member

It would surprise me if you were using tweak_add in any way that the fuzzer would've had difficulty with, since the original code was not consistent

I'd buy that it meant our fuzzers have been bad, but still need to be able to call it.

One interesting thing we could try would be to compile libsecp with an order-13 group (rather than the ~2^256-order group that we normally use). And then not mock any functions.

Hmm, this may make sense - forcing the fuzzer to guess two bytes instead of one kinda sucks, but if it means we get exact behavior maybe its still worth it.

@apoelstra
Copy link
Member Author

You can call everything in the library now with this PR. Before you couldn't because many functions were unimplemented!().

@TheBlueMatt
Copy link
Member

Oh, oops, I misread that as order-2^13, not order-13, maybe lets just do that?

@apoelstra
Copy link
Member Author

Will need a couple hours to play with this -- our order-13 implementation apparently rejects secret keys outside of the range [1, 12] so I'm seeing if I can work around that or if I should just patch the vendored copy of rust-secp to be more forgiving :P.

@apoelstra
Copy link
Member Author

Hmmm. So, I can definitely do the order-13 thing but it breaks a lot of our unit tests because half the valid pubkeys on secp are invalid on secp-13, and vice-versa.

I can change the overflow logic to check against the secp curve order,, or I can be more clever (and say, reject half of all keys). The former would mean fewer unit tests break, but the latter would give us better fuzz-test coverage.

No matter what, fuzz test failures are going to be weird and confusing since all of the public curvepoints will live on a different curve from secp.

@apoelstra
Copy link
Member Author

But even with all these considerations, I think we should do it. What do you think @elichai @TheBlueMatt?

To be clear, my proposal is:

  • Rather than mocking functionality in the test lib, have the rust_secp_fuzz feature enable the upstream EXHAUSTIVE_TEST_ORDER=13, which switches libsecp to work on a different EC group of order 13.
  • Modify the overflow logic to complain about secret keys in excess of 0f00000....0000 say, rather than either 13 or the actual secp order. This means that the fuzzer will be able to easily find out-of-range keys even if it's getting them from hash functions.
  • Disable something like 50% of all the unit tests when the fuzz feature is enabled since almost no fixed-vector tests (for secret keys or public keys) will work anymore.

By using a group of order 13 but allowing secret keys > 13, we break some invariants ... such as there being only one secret key corresponding to each public key and the negate function on secret keys not giving you the original secret key when you run it twice.

I think we should try it, and if shit hits the fan we'll just return to the proposal from this PR (where almost all unit tests pass so we know it's being sane) in a patch release.

@TheBlueMatt
Copy link
Member

TheBlueMatt commented Dec 23, 2020

This means that the fuzzer will be able to easily find out-of-range keys even if it's getting them from hash functions.

I don't think this is a good idea. Its generally not unreasonable to panic if a hash function generates an invalid secp private key, whereas suddenly it will be commonplace.

Otherwise I tend to agree.

@apoelstra
Copy link
Member Author

Yeah, that's reasonable. I'll change it to check against the actual secp order (or at least, something so big that it's cryptographically inaccessible).

@apoelstra
Copy link
Member Author

Added a commit which demonstrates what this'd look like. Currently the tests fail because schnorr signing fails a high proportion of the time when you run it on a compromised group (since BIP340 was defined assuming that out-of-range nonces are cryptographically inaccessible). Also I directly edited the vendored copy of libsecp rather than adding a patch to the vendor script, which I need to fix before this is mergeable.

@apoelstra
Copy link
Member Author

Hmm, CI should not have passed there. Looks like it's not actually running the test matrix. Sigh.

@apoelstra
Copy link
Member Author

Removed the order-13 commit. We should revisit this in the future but it'll take a fair bit more work than just enabling the order-13 group.

Let's try to get this in now so we can have a release.

) -> c_int {
let sig_sl = slice::from_raw_parts(sig as *const u8, 65);
let msg_sl = slice::from_raw_parts(msg32 as *const u8, 32);
println!("HMM0");
Copy link
Member

Choose a reason for hiding this comment

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

You forgot this print here

Comment on lines +108 to +109
sig_sl.swap(32, 64);
sig_sl[64] -= 2;
Copy link
Member

Choose a reason for hiding this comment

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

Oh you're using the pubkey parity(0x02/0x03) for a Recover parity, clever :)

let msg_sl = slice::from_raw_parts(msg32 as *const u8, 32);
println!("HMM0");

if sig_sl[64] > 4 {
Copy link
Member

Choose a reason for hiding this comment

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

I think that should be >= 4, because the VERIFY_CHECK makes sure that recid < 4, so it should return 0 on 4.

Copy link
Member

@elichai elichai left a comment

Choose a reason for hiding this comment

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

utACK

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.

None yet

3 participants