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

Switch from sjcl to noble-curves and noble-hashes #26

Closed
wants to merge 7 commits into from
Closed

Switch from sjcl to noble-curves and noble-hashes #26

wants to merge 7 commits into from

Conversation

paulmillr
Copy link

@paulmillr paulmillr commented Feb 25, 2023

I have removed InnerScalar hack because private property access (s.k) may fail at some point in the future when typescript will start compiling it to actual private property.

Benchmark results on M2 / node v18.14.0. Before:

OPRF/P256_SHA256/blind     x 43.97 ops/sec ±0.10% (72 runs sampled)
OPRF/P256_SHA256/blindEval x 121 ops/sec ±0.23% (83 runs sampled)
OPRF/P256_SHA256/finalize  x 121 ops/sec ±0.17% (83 runs sampled)
OPRF/P384_SHA384/blind     x 31.72 ops/sec ±0.25% (77 runs sampled)
OPRF/P384_SHA384/blindEval x 93.83 ops/sec ±0.12% (88 runs sampled)
OPRF/P384_SHA384/finalize  x 92.29 ops/sec ±0.18% (87 runs sampled)
OPRF/P521_SHA512/blind     x 14.72 ops/sec ±0.11% (72 runs sampled)
OPRF/P521_SHA512/blindEval x 46.10 ops/sec ±0.16% (75 runs sampled)
OPRF/P521_SHA512/finalize  x 44.52 ops/sec ±4.16% (74 runs sampled)
VOPRF/P256_SHA256/blind     x 44.08 ops/sec ±0.36% (72 runs sampled)
VOPRF/P256_SHA256/blindEval x 15.83 ops/sec ±2.53% (76 runs sampled)
VOPRF/P256_SHA256/finalize  x 17.95 ops/sec ±2.55% (17 runs sampled)
VOPRF/P384_SHA384/blind     x 30.59 ops/sec ±2.33% (74 runs sampled)
VOPRF/P384_SHA384/blindEval x 11.87 ops/sec ±3.11% (59 runs sampled)
VOPRF/P384_SHA384/finalize  x 14.56 ops/sec ±0.66% (16 runs sampled)
VOPRF/P521_SHA512/blind     x 14.37 ops/sec ±1.42% (70 runs sampled)
VOPRF/P521_SHA512/blindEval x 6.02 ops/sec ±0.57% (34 runs sampled)
VOPRF/P521_SHA512/finalize  x 12.10 ops/sec ±12.39% (15 runs sampled)
POPRF/P256_SHA256/blind     x 44.12 ops/sec ±0.10% (72 runs sampled)
POPRF/P256_SHA256/blindEval x 14.00 ops/sec ±0.54% (69 runs sampled)
POPRF/P256_SHA256/finalize  x 15.67 ops/sec ±0.54% (24 runs sampled)
POPRF/P384_SHA384/blind     x 31.66 ops/sec ±0.29% (76 runs sampled)
POPRF/P384_SHA384/blindEval x 10.39 ops/sec ±0.49% (53 runs sampled)
POPRF/P384_SHA384/finalize  x 11.62 ops/sec ±0.19% (19 runs sampled)
POPRF/P521_SHA512/blind     x 14.69 ops/sec ±0.11% (72 runs sampled)
POPRF/P521_SHA512/blindEval x 4.95 ops/sec ±0.72% (28 runs sampled)
POPRF/P521_SHA512/finalize  x 6.82 ops/sec ±0.33% (15 runs sampled)
P-256/add          x 1,321 ops/sec ±0.26% (99 runs sampled)
P-256/mulgen       x 123 ops/sec ±0.13% (80 runs sampled)
P-256/mul          x 122 ops/sec ±0.25% (80 runs sampled)
P-256/hashToScalar x 15,058 ops/sec ±0.22% (89 runs sampled)
P-256/hashToGroup  x 258 ops/sec ±0.68% (87 runs sampled)
P-384/add          x 923 ops/sec ±0.25% (99 runs sampled)
P-384/mulgen       x 92.53 ops/sec ±0.61% (81 runs sampled)
P-384/mul          x 93.20 ops/sec ±0.22% (81 runs sampled)
P-384/hashToScalar x 9,452 ops/sec ±0.55% (89 runs sampled)
P-384/hashToGroup  x 183 ops/sec ±0.26% (87 runs sampled)
P-521/add          x 409 ops/sec ±0.40% (96 runs sampled)
P-521/mulgen       x 44.35 ops/sec ±2.40% (59 runs sampled)
P-521/mul          x 45.12 ops/sec ±2.44% (61 runs sampled)
P-521/hashToScalar x 5,979 ops/sec ±0.90% (87 runs sampled)
P-521/hashToGroup  x 81.83 ops/sec ±0.27% (79 runs sampled)

After:

OPRF/P256_SHA256/blind     x 376 ops/sec ±0.18% (89 runs sampled)
OPRF/P256_SHA256/blindEval x 424 ops/sec ±0.26% (90 runs sampled)
OPRF/P256_SHA256/finalize  x 416 ops/sec ±0.22% (88 runs sampled)
OPRF/P384_SHA384/blind     x 158 ops/sec ±0.21% (84 runs sampled)
OPRF/P384_SHA384/blindEval x 179 ops/sec ±0.33% (84 runs sampled)
OPRF/P384_SHA384/finalize  x 177 ops/sec ±0.22% (92 runs sampled)
OPRF/P521_SHA512/blind     x 82.92 ops/sec ±0.33% (80 runs sampled)
OPRF/P521_SHA512/blindEval x 94.02 ops/sec ±0.39% (89 runs sampled)
OPRF/P521_SHA512/finalize  x 92.93 ops/sec ±0.39% (88 runs sampled)
VOPRF/P256_SHA256/blind     x 375 ops/sec ±0.20% (92 runs sampled)
VOPRF/P256_SHA256/blindEval x 100 ops/sec ±0.23% (80 runs sampled)
VOPRF/P256_SHA256/finalize  x 77.26 ops/sec ±0.27% (91 runs sampled)
VOPRF/P384_SHA384/blind     x 158 ops/sec ±0.26% (92 runs sampled)
VOPRF/P384_SHA384/blindEval x 42.37 ops/sec ±0.39% (69 runs sampled)
VOPRF/P384_SHA384/finalize  x 32.92 ops/sec ±0.30% (79 runs sampled)
VOPRF/P521_SHA512/blind     x 82.71 ops/sec ±0.31% (79 runs sampled)
VOPRF/P521_SHA512/blindEval x 22.33 ops/sec ±0.54% (57 runs sampled)
VOPRF/P521_SHA512/finalize  x 17.49 ops/sec ±0.47% (83 runs sampled)
POPRF/P256_SHA256/blind     x 376 ops/sec ±0.16% (92 runs sampled)
POPRF/P256_SHA256/blindEval x 100 ops/sec ±0.13% (80 runs sampled)
POPRF/P256_SHA256/finalize  x 75.90 ops/sec ±0.22% (89 runs sampled)
POPRF/P384_SHA384/blind     x 158 ops/sec ±0.18% (92 runs sampled)
POPRF/P384_SHA384/blindEval x 42.39 ops/sec ±0.29% (69 runs sampled)
POPRF/P384_SHA384/finalize  x 32.05 ops/sec ±0.39% (77 runs sampled)
POPRF/P521_SHA512/blind     x 82.72 ops/sec ±0.32% (79 runs sampled)
POPRF/P521_SHA512/blindEval x 22.21 ops/sec ±0.56% (56 runs sampled)
POPRF/P521_SHA512/finalize  x 17.03 ops/sec ±0.37% (81 runs sampled)
P-256/add          x 257,802 ops/sec ±0.16% (99 runs sampled)
P-256/mulgen       x 5,365 ops/sec ±0.07% (102 runs sampled)
P-256/mul          x 423 ops/sec ±0.25% (95 runs sampled)
P-256/hashToScalar x 83,171 ops/sec ±0.08% (92 runs sampled)
P-256/hashToGroup  x 3,320 ops/sec ±0.18% (92 runs sampled)
P-384/add          x 173,196 ops/sec ±0.61% (99 runs sampled)
P-384/mulgen       x 2,430 ops/sec ±0.66% (98 runs sampled)
P-384/mul          x 175 ops/sec ±0.55% (91 runs sampled)
P-384/hashToScalar x 44,492 ops/sec ±2.01% (86 runs sampled)
P-384/hashToGroup  x 1,364 ops/sec ±0.19% (93 runs sampled)
P-521/add          x 126,775 ops/sec ±0.30% (100 runs sampled)
P-521/mulgen       x 1,324 ops/sec ±0.18% (100 runs sampled)
P-521/mul          x 91.47 ops/sec ±0.46% (80 runs sampled)
P-521/hashToScalar x 44,770 ops/sec ±0.05% (93 runs sampled)
P-521/hashToGroup  x 686 ops/sec ±0.47% (90 runs sampled)

@paulmillr paulmillr mentioned this pull request Feb 25, 2023
@armfazh armfazh self-assigned this Feb 26, 2023
src/group.ts Outdated Show resolved Hide resolved
src/group.ts Show resolved Hide resolved
src/group.ts Outdated Show resolved Hide resolved
src/group.ts Show resolved Hide resolved
@paulmillr
Copy link
Author

I have tried implementing ristretto, but it takes a lot of time. There's little-endian instead of big-endian and bit fiddling (clearing bits in scalars), etc.

Noble curves with DST that accepts Uint8Array should be out later. Other than that, the PR is ready for review.

package.json Outdated Show resolved Hide resolved
Co-authored-by: Armando Faz <armfazh@users.noreply.github.com>
Copy link

@cjpatton cjpatton left a comment

Choose a reason for hiding this comment

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

Thanks @paulmillr for the contribution! These look like great libraries. After having a closer look at noble-curves (I have not had a chance to look at noble-hashes yet), the selling points of these implementations are indeed quite attractive.

My initial concern is that there was no effort to make the field arithmetic constant-time. As the README rightly points out, we can't reasonably expect a JS implementation of things like scalar multiplication to mitigate all possible side channels (e.g., an attacker that shares CPU cache with the process). That said, it may be reasonable to expect such code to resist less sophisticated threats (e.g., an attacker making requests to the process from the network). Put simply: orders of magnitude matter.

Before taking this PR, I'd like to suggest we assess whether there are regressions in side-channel resistance. In particular, if the SJCL made any effort to make the field arithmetic constant time, then I think we should keep this dependency for now.

I should emphasize however that I'm not really an expert on how to write low-level crypto securely (@armfazh is) and I'm not an expert in JS. Which leads me to a question: What accounts for the speed up of noble-curves over SJCL? (E.g., nearly 3.5x for P-256 scalar multiplication, if I'm reading your benchmarks correctly.) Perhaps there side-channel mitigations built into SJCL's field arithmetic that make it inherently slower?

@paulmillr
Copy link
Author

paulmillr commented Mar 2, 2023

No. Sjcl does not have ct mitigations. Noble curves do. Sjcl is basically unsupported buggy garbage these days. Maybe they were great in 2012.

What accounts for the speed up of noble-curves over SJCL

Native bigint versus non native bigint (which is also non ct) and tons of other hand-picked optimizations

@paulmillr
Copy link
Author

paulmillr commented Mar 2, 2023

My initial concern is that there was no effort to make the field arithmetic constant-time

Bugs:

You're saying "noble is bad" because we don't have ct fields (sjcl fields are also not ct), however, noble has things which are a lot more important: algorithmically CT multiplication using second accumulator, complete addition formulas. Just do some EC ct tests, you'll see what is where.

@cjpatton
Copy link

cjpatton commented Mar 2, 2023

You're saying "noble is bad" because we don't have ct fields (sjcl fields are also not ct), however, noble has things which are a lot more important: algorithmically CT multiplication using second accumulator, complete addition formulas. Just do some EC ct tests, you'll see what is where.

To be clear I'm not suggesting noble is bad. My questions were meant to assess whether we would lose any CT code by taking this change, which you are saying we don't. I appreciate you taking the time to point this out.

@bwesterb
Copy link
Member

bwesterb commented Mar 2, 2023

I haven't studied the topic constant-time javascript in much detail yet, but it scares me. For instance, JITs commonly specialise functions on commonly used arguments, and that's a bummer if the argument is meant to be secret.

With that big caveat out of the way: noble relies on BigInt, which does not have any constant time guarantees. There are javascript libaries that claim constant-timeness. What if we swap out BigNum for the one from this library.

@paulmillr
Copy link
Author

paulmillr commented Mar 2, 2023

@bwesterb I wrote this paragraph on MDN. Pull request 20272 in repository mdn/content.

@paulmillr
Copy link
Author

Folks, you literally have dumpster fire right now with regards to timing attacks (curves themselves) and you're talking about some theoretical nonsense. Did you check the library you've linked? There is no CT inversion there. This doesn't make any sense.

@chris-wood
Copy link

Thanks @paulmillr for taking the time to prepare and send this PR! We're investigating this change and will get back to this thread when we have time. Please be patient. In the meantime, if you could clean up conflicts, that would help make our assessment easier. Please also be mindful of our code of conduct when engaging here. Let's keep the discussion and tone professional and focus on the technical matters at hand. We look forward to your continued, constructive contributions!

@paulmillr
Copy link
Author

Understood.

To make decision easier for you, check out how sjcl calculation time depends on private key bits, effectively leaking all the timings:

sjcl private key A x 7,624 ops/sec @ 131μs/op ± 7.36% (min: 74μs, max: 313μs)
sjcl private key B x 117 ops/sec @ 8ms/op
sjcl private key C x 56 ops/sec @ 17ms/op

Reproducible with this code:

// npm init -y && npm install micro-bmark sjcl-including-ecc
const bmark = require('micro-bmark');
const sjcl = require('sjcl-including-ecc');
const curve = sjcl.ecc.curves.k256;
// Generate new key
// var k2 = sjcl.ecc.ecdsa.generateKeys(curve, 0); console.log(k2.sec.serialize())
const privA = '1000000000000000000000000000000000000000000000000000000000000000';
const privB = '0000000000000000000000000000010000000000000000000000000000000000';
const privC = '0000000000000000000000000000000000000000000000000000000000000001';
bmark.run(async () => {
  console.log(curve.G.mult(privA).isIdentity);
  await bmark.mark('sjcl private key A', 110, () => curve.G.mult(privA));
  await bmark.mark('sjcl private key B', 110, () => curve.G.mult(privB));
  await bmark.mark('sjcl private key C', 110, () => curve.G.mult(privC));
})

This is catastrophically bad result and it won't happen with curves. By the way, curves got audited, I will publish it in the repo asap.

@paulmillr
Copy link
Author

audit is live now:

https://github.com/trailofbits/publications/blob/master/reviews/2023-01-ryanshea-noblecurveslibrary-securityreview.pdf

@paulmillr
Copy link
Author

It has been around 1 month since the PR was created. I have shown you how your current cryptography is not secure and is bleeding with vulnerability leaking timings that could be exploited today, given enough resources. On a contrary, curves were audited by a third party.

Is this going to be reviewed, or did I do some unnecessary investment of 7 hours of free work in cloudflare repository that you've agreed upon before?

This is not how open source is developed. Either you don't want it and say in advance, or at least leave some feedback to OSS contributors.

@chris-wood
Copy link

As I said above, @paulmillr, we are still investigating this PR amongst many other competing priorities. Please be patient. Being disrespectful will not expedite the process.

@paulmillr
Copy link
Author

sjcl will be crushed by noble-curves any day now. Watch for weekly downloads and dependents who've made their pick at this url:

@sublimator
Copy link
Contributor

This seems a clear improvement. An MIT licensed (can always fork) open source lib by a passionate author vs an ancient lib that requires vendoring and patching. Not to mention the clear performance gains!

@armfazh
Copy link
Contributor

armfazh commented Jun 19, 2023

Update: in #35 , there is a new way to make the group implementation plug-able. This enable users to provide a different (than sjcl) implementation.

@sublimator
Copy link
Contributor

@armfazh

Would you be fine with bundling a @noble implementation in this package? (and updating the test files to test it)

Can prefix it with experimental (to start?)
Something like: "import { NobleGroup } from 'voprf-ts/experimental-noble-group'"

Also, while I'm at it messing with package.json "exports", I would like to sort out commonjs support (see #30 )

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.

6 participants