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

feat: implement GroupNoble #38

Merged
merged 2 commits into from
Sep 2, 2023
Merged

Conversation

sublimator
Copy link
Contributor

@sublimator sublimator commented Jun 20, 2023

Continuing on with #26
Also deals with #30

Scripts

Test Type All Groups Specific Group (For performance while testing)
All Tests npm test GROUP_CONS=noble npm test
ESM Tests npm run test:esm GROUP_CONS=noble npm run test:esm
CJS Tests npm run test:cjs GROUP_CONS=noble npm run test:cjs

Notes

  • The Oprf.Group member is [re]configured by a describeGroupTests helper that takes a declare function and uses describe.each
    • The library itself does not honour the env variable.
  • @noble/hashes and @noble/curves are listed as "optionalDependencies" (also "devDependencies" for tests) as not strictly required to use the library
  • Use Group.fromID factory instead of Group new constructor (eye to short vs ristretto/decaf448 etc)

src/index.ts Outdated Show resolved Hide resolved
test/server.test.ts Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
src/groupNoble.ts Outdated Show resolved Hide resolved
src/groupNoble.ts Outdated Show resolved Hide resolved
src/groupNoble.ts Outdated Show resolved Hide resolved
src/groupNoble.ts Outdated Show resolved Hide resolved
@sublimator
Copy link
Contributor Author

sublimator commented Jun 20, 2023 via email

src/groupNoble.ts Outdated Show resolved Hide resolved
@sublimator
Copy link
Contributor Author

@armfazh

I added some tsconfig.cjs.json files, and added them as a reference to the root tsconfig.json.
However it slows things down considerably during development.

I also note that this is declaring "type": "module" in the package.json, which, due to node module resolution means any consumers will have to be using ESM, regardless of any "exports" configuration:


(base) ➜  voprf-ts git:(nd-wip-2023-06-20) ✗ node
Welcome to Node.js v18.16.0.
Type ".help" for more information.
> require('.')
Uncaught:
Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/nicholasdudfield/projects/voprf-ts/lib/cjs/src/index.js not supported.
index.js is treated as an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which declares all .js files in that package scope as ES modules.
Instead rename index.js to end in .cjs, change the requiring code to use dynamic import() which is available in all CommonJS modules, or change "type": "module" to "type": "commonjs" in /Users/nicholasdudfield/projects/voprf-ts/package.json to treat all .js files as CommonJS (using .mjs for all ES modules instead).

    at __node_internal_captureLargerStackTrace (node:internal/errors:490:5)
    at new NodeError (node:internal/errors:399:5)
    at Module._extensions..js (node:internal/modules/cjs/loader:1281:19)
    at Module.load (node:internal/modules/cjs/loader:1117:32)
    at Module._load (node:internal/modules/cjs/loader:958:12)
    at Module.require (node:internal/modules/cjs/loader:1141:19)
    at require (node:internal/modules/cjs/helpers:110:18) {
  code: 'ERR_REQUIRE_ESM'

I tried neutralising the package.json by removing "type":"module", however jest started failing as it didn't know to interpret .js files as ESM:

    Details:

    /Users/nicholasdudfield/projects/voprf-ts/lib/test/vectors.test.js:5
    import { derivePrivateKey, generatePublicKey, Oprf, OPRFClient, OPRFServer, POPRFClient, POPRFServer, VOPRFClient, VOPRFServer } from '../src/index.js';
    ^^^^^^

    SyntaxError: Cannot use import statement outside a module

I will try again later, but if you have any ideas?

Thinking out loud:
Perhaps there could be a separate cjs build step to create a cjs package.json (minus "type": "module"), compile all the code (including the test code) also as cjs, and a cjs specific test command ? Otherwise, there needs to be some way of configuring jest to run the tests without "type": "module" ?

@sublimator
Copy link
Contributor Author

sublimator commented Jun 24, 2023

@armfazh

Ok, I've got something that seems to work. I removed "type" : "module" from the root package.json and committed lib/{esm,cjs}/package.json files setting their respective "type"s. At the moment the test/build commands simply build and test both cjs and esm at once (edit: added commands for faster specific tests). SCJL is the default group, and the tests can be configured to test noble by setting the environment variable GROUP_CONS to noble

Test Type Default Group Noble Group
All Tests npm test GROUP_CONS=noble npm test
ESM Tests npm run test:esm GROUP_CONS=noble npm run test:esm
CJS Tests npm run test:cjs GROUP_CONS=noble npm run test:cjs

@sublimator sublimator marked this pull request as ready for review June 26, 2023 23:55
@sublimator
Copy link
Contributor Author

@armfazh @lukevalenta

?

Copy link
Contributor

@armfazh armfazh left a comment

Choose a reason for hiding this comment

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

Hi, here are some suggested changes.

package.json Show resolved Hide resolved
src/groupNoble.ts Outdated Show resolved Hide resolved
compat(this, k2)
compat(this, a)
const el = this.p.multiplyAndAddUnsafe(a.p, k1.k, k2.k)
if (!el) throw new Error('result is zero')
Copy link
Contributor

Choose a reason for hiding this comment

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

it shouldn't make a special case returning the ZERO point.

For example this is valid call to this function:

P = gen()
Q = P
a = order-1
b = 1

R = a*P+b*Q
  = (order-1)*P + P
  = -P+P
  = 0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

    /**
     * Efficiently calculate `aP + bQ`. Unsafe, can expose private key, if used incorrectly.
     * Not using Strauss-Shamir trick: precomputation tables are faster.
     * The trick could be useful if both P and Q are not G (not in our case).
     * @returns non-zero affine point
     */
    multiplyAndAddUnsafe(Q: Point, a: bigint, b: bigint): Point | undefined {
      const G = Point.BASE; // No Strauss-Shamir trick: we have 10% faster G precomputes
      const mul = (
        P: Point,
        a: bigint // Select faster multiply() method
      ) => (a === _0n || a === _1n || !P.equals(G) ? P.multiplyUnsafe(a) : P.multiply(a));
      const sum = mul(this, a).add(mul(Q, b));
      return sum.is0() ? undefined : sum;
    }

It seems multiplyAndAddUnsafe returns a non zero Point

This one is probably better fielded by @paulmillr

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the moment I'm just returning a zero point when multiplyAndAddUnsafe returns undefined (the point was zero)

Any better ideas ?

Copy link
Contributor

Choose a reason for hiding this comment

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

it seems to me that this need to be solved in the noble package.

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 workaround should be OK to get things moving though, no?

src/groupNoble.ts Outdated Show resolved Hide resolved
return EltNb.gen(this).mul(s)
}

async randomScalar(): Promise<ScalarNb> {
Copy link
Contributor

Choose a reason for hiding this comment

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

no need to use async as no await is in the body of this function.

Suggested change
async randomScalar(): Promise<ScalarNb> {
randomScalar(): Promise<ScalarNb> {

same applies to the other methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Some linters actually enforce consistent use of async, which is something I can get behind, but if that's the convention here, I can go with it. Maybe there's a linting rule this project could set up ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

async randomScalar(): Promise<ScalarNb> {
    const msg = crypto.getRandomValues(new Uint8Array(this.size))
    return ScalarNb.hash(this, msg, new Uint8Array())
}

Actually, looking at this more, I can see that the async modifier is what makes the simple return value upgrade to a Promise, rather than having to use Promise.resolve(value)

Our Group interfaces are async (using Promises), so the async is actually required?

Copy link
Contributor

Choose a reason for hiding this comment

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

until there is a linter rule enabled to get this uniformly, I consider that using async only when inside the body of the function there is an await keyword.

A function is known to be async iff it returns a Promise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A function is known to be async iff it returns a Promise.

Let's dig into this a bit more.

The randomScalar method of our Group interface is asynchronous so that it can support the sjcl/webcrypto (used for expandMD) implementation.

scalarNb.hash(this, msg, new Uint8Array())

This is a sync operation, and not a promise, it's true, but by putting async on the function it "upgrades" the return value to a Promise so it will conform to our interface.

The alternative here is to use Promise.resolve(), which I will do:

    randomScalar(): Promise<ScalarNb> {
        const msg = crypto.getRandomValues(new Uint8Array(this.size))
        return Promise.resolve(ScalarNb.hash(this, msg, new Uint8Array()))
    }

src/sjcl/index.js Outdated Show resolved Hide resolved
test/dleq.test.ts Outdated Show resolved Hide resolved
test/dleq.test.ts Outdated Show resolved Hide resolved
test/setGroup.ts Outdated Show resolved Hide resolved
.github/workflows/node.yml Outdated Show resolved Hide resolved
@sublimator
Copy link
Contributor Author

sublimator commented Jul 19, 2023 via email

@sublimator
Copy link
Contributor Author

@armfazh

I've addressed your concerns/comments. I've left the for static method for now, in
anticipation of separate Group/Elt/Scalar classes used by decaf/ristretto.

Thanks!

src/groupNoble.ts Outdated Show resolved Hide resolved
return EltNb.gen(this).mul(s)
}

async randomScalar(): Promise<ScalarNb> {
Copy link
Contributor

Choose a reason for hiding this comment

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

until there is a linter rule enabled to get this uniformly, I consider that using async only when inside the body of the function there is an await keyword.

A function is known to be async iff it returns a Promise.

Comment on lines 303 to 319
eltDes: Deserializer<EltNb> = {
size: (compressed?: boolean) => {
return this.eltSize(compressed)
},
deserialize: (b: Uint8Array) => {
return this.desElt(b)
}
}
scalarDes: Deserializer<ScalarNb> = {
size: () => {
return this.scalarSize()
},
deserialize: (b: Uint8Array) => {
return this.desScalar(b)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

bump

src/groupSjcl.ts Outdated Show resolved Hide resolved
src/groupTypes.ts Outdated Show resolved Hide resolved
@armfazh
Copy link
Contributor

armfazh commented Sep 1, 2023

@sublimator please rebase and squash your commits, so it can be merged.

@sublimator
Copy link
Contributor Author

I got conflicts when I tried to rebase, so I just merged origin/main, so it should squash down from the Github UI easily.

Only merge commits allowed?

@sublimator
Copy link
Contributor Author

Ok, I created a new commit on top of main from the contents of the old commits and force pushed.

@armfazh armfazh merged commit 44f96ab into cloudflare:main Sep 2, 2023
3 checks passed
@armfazh
Copy link
Contributor

armfazh commented Sep 2, 2023

Thank for your contributions @sublimator

@sublimator
Copy link
Contributor Author

sublimator commented Sep 2, 2023 via email

@armfazh armfazh mentioned this pull request Sep 2, 2023
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.

2 participants