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(sdk-lib-mpc): add implementation of paillier-blum #3940

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d7b4a64
feat(sdk-lib-mpc): add implementation of paillier-blum for paillier p…
johnoliverdriscoll Jul 28, 2023
c4315e8
feat(sdk-lib-mpc): add types for paillierBlumProof methods
zahin-mohammad Aug 14, 2023
b4f6afb
chore(sdk-lib-mpc): use triple =
zahin-mohammad Aug 14, 2023
1f0d3ca
chore(sdk-lib-mpc): rename files to *Proof
zahin-mohammad Aug 14, 2023
28e3fd5
test(sdk-lib-mpc): add paillierBlumProof tests
zahin-mohammad Aug 16, 2023
820f19f
chore(sdk-lib-mpc): rename files to temp
zahin-mohammad Aug 16, 2023
d0f701a
chore(sdk-lib-mpc): remove temp files
zahin-mohammad Aug 16, 2023
29bb06f
chore(sdk-lib-mpc): fix fmt
zahin-mohammad Aug 16, 2023
9fcf9cb
chore(sdk-lib-mpc): add reference to paillierBlum
zahin-mohammad Aug 24, 2023
296af04
chore(sdk-lib-mpc): use shake256 and sampling rejection for generatin…
johnoliverdriscoll Aug 30, 2023
a618484
feat(sdk-lib-mpc): add noSmallFactors module
johnoliverdriscoll Sep 11, 2023
74cd01b
feat(sdk-lib-mpc): rename nsf.sigma to nsf.rho to match whitepaper
johnoliverdriscoll Sep 20, 2023
ed1bf0c
chore(sdk-lib-mpc): use xit instead of comment
zahin-mohammad Sep 27, 2023
c0eda1f
chore(sdk-lib-mpc): regenerate fixtures
zahin-mohammad Sep 27, 2023
4b20288
chore(sdk-lib-mpc): increase timeout
zahin-mohammad Sep 27, 2023
7e3836e
chore(sdk-lib-mpc): update rangeProof import
zahin-mohammad Sep 27, 2023
850e86a
chore(sdk-lib-mpc): update paillierProof import
zahin-mohammad Sep 27, 2023
e3ff77c
chore(sdk-lib-mpc): skip prove and verify
zahin-mohammad Sep 27, 2023
244d72e
chore(sdk-lib-mpc): prettier
zahin-mohammad Sep 27, 2023
519e200
chore(sdk-lib-mpc): increase timeout for prove tests
zahin-mohammad Sep 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/sdk-lib-mpc/.mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module.exports = {
require: 'ts-node/register',
timeout: '20000',
timeout: '40000',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bitgo/sdk-lib-mpc:          creating paillier blum proof should not throw an error, test case 0:
@bitgo/sdk-lib-mpc:      Error: Timeout of 20000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/runner/work/BitGoJS/BitGoJS/modules/sdk-lib-mpc/test/unit/tss/ecdsa/paillierBlumProof.ts)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will need to be optimized in a follow up.

reporter: 'min',
'reporter-option': ['cdn=true', 'json=false'],
exit: true,
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-lib-mpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"build": "yarn tsc --build --incremental --verbose .",
"fmt": "prettier --write .",
"check-fmt": "prettier --check .",
"fix-fmt": "prettier --write .",
"clean": "rm -r ./dist",
"lint": "eslint --quiet .",
"prepare": "npm run build"
Expand Down
32 changes: 32 additions & 0 deletions modules/sdk-lib-mpc/src/tss/ecdsa/generatePaillierKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as bcu from 'bigint-crypto-utils';
import { PublicKey, PrivateKey, KeyPair } from 'paillier-bigint';
import { prove } from './paillierBlumProof';
import { DeserializedKeyPairWithPaillierBlumProof, RawPaillierKey } from './types';

// Implementation based on paillier-bigint's generateRandomKeys
export async function generatePaillierKey(bitlength = 3072): Promise<RawPaillierKey> {
let [p, q, n] = [BigInt(0), BigInt(0), BigInt(0)];
do {
p = await bcu.prime(Math.floor(bitlength / 2) + 1);
q = await bcu.prime(Math.floor(bitlength / 2));
n = p * q;
} while (q === p || q % BigInt(4) !== BigInt(3) || p % BigInt(4) !== BigInt(3) || bcu.bitLength(n) !== bitlength);
const lambda = (p - BigInt(1)) * (q - BigInt(1));
const mu = bcu.modInv(lambda, n);
return { n, lambda, mu, p, q };
}

export async function generatePaillierKeyWithProof(
bitlength = 3072
): Promise<DeserializedKeyPairWithPaillierBlumProof> {
const key = await generatePaillierKey(bitlength);
const proof = await prove(key.p, key.q);
return { ...key, ...proof };
}

export function rawPaillierKeyToPaillierKey(n: bigint, lambda: bigint, mu: bigint, p: bigint, q: bigint): KeyPair {
const g = n + BigInt(1);
const publicKey = new PublicKey(n, g);
const privateKey = new PrivateKey(lambda, mu, publicKey, p, q);
return { publicKey, privateKey };
}
7 changes: 4 additions & 3 deletions modules/sdk-lib-mpc/src/tss/ecdsa/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * as EcdsaTypes from './types';
export * as EcdsaRangeProof from './rangeproof';
export * as EcdsaPaillierProof from './paillierproof';
export * as EcdsaRangeProof from './rangeProof';
export * as EcdsaPaillierProof from './paillierProof';
export * as EcdsaZkVProof from './zkVProof';

export * as EcdsaNoSmallFactorsProof from './noSmallFactorsProof';
export * from './generatePaillierKey';
export const minModulusBitLength = 3072;
145 changes: 145 additions & 0 deletions modules/sdk-lib-mpc/src/tss/ecdsa/noSmallFactorsProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Implementation of No Small Factors ($\Pi^\text{fac}).
* https://eprint.iacr.org/2020/492.pdf Section B.4
*/
import { createHash } from 'crypto';
import { bitLength, randBetween } from 'bigint-crypto-utils';
import { modPow } from 'bigint-mod-arith';
import { bigIntFromBufferBE, bigIntToBufferBE } from '../../util';
import { DeserializedNoSmallFactorsProof } from './types';

const ORDER = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
const ELL = BigInt(256);
const EPSILON = BigInt(BigInt(2) * ELL);

/**
* Generate pseudo-random challenge value $e$ for $(N, w)$.
* @param N - the prime number to verify is a product of two large primes.
* @param w - a random number with the same bitLength as N, that satisfies the Jacobi of w is -1 wrt N.
* @returns {bigint} - challenge value $e$.
*/
function generateE(N: bigint, w: bigint): bigint {
const digest = createHash('shake256', { outputLength: 1 + Math.floor((bitLength(ORDER) + 7) / 8) })
.update(bigIntToBufferBE(N))
.update('$')
.update(bigIntToBufferBE(w))
.update('$')
.digest();
const e = bigIntFromBufferBE(digest.subarray(1)) % ORDER;
if (digest[0] & 1) {
return -e;
}
return e;
}

/**
* Calculate the closest integer square root of $n$.
* @param n - the number to calculate the square root of.
* @returns {bigint} - $n$'s closest integer square root.
*/
function isqrt(n: bigint): bigint {
if (n < BigInt(0)) {
throw new Error();
}
if (n < BigInt(2)) {
return n;
}
function newtonIteration(n: bigint, x0: bigint) {
const x1 = (n / x0 + x0) >> BigInt(1);
if (x0 === x1 || x0 === x1 - BigInt(1)) {
return x0;
}
return newtonIteration(n, x1);
}
return newtonIteration(n, BigInt(1));
}

/**
* Prove that $n0$ has no small factors, where $n0$ is the product of two large primes.
* @param p - a large prime.
* @param q - a large prime.
* @param w - a random number with the same bitLength as $p * q$, that satisfies the Jacobi of w is -1 wrt $n0$.
* @param nHat - a safe bi-prime, such as that returned from rangeProof.generateNTilde.
* @param s - security parameters for $nHat$ such as the $h1$ value returned from rangeProof.generateNTilde.
* @param t - security parameters for $nHat$ such as the $h2$ value returned from rangeProof.generateNTilde.
* @returns proof that the product of $p * q$ has no small factors.
*/
export function prove(
p: bigint,
q: bigint,
w: bigint,
nHat: bigint,
s: bigint,
t: bigint
): DeserializedNoSmallFactorsProof {
const n0 = p * q;
const e = generateE(n0, w);
const sqrtN0 = isqrt(n0);
const alpha = randBetween(sqrtN0 << (ELL + EPSILON), -sqrtN0 << (ELL + EPSILON));
const beta = randBetween(sqrtN0 << (ELL + EPSILON), -sqrtN0 << (ELL + EPSILON));
const rho = randBetween((nHat * n0) << ELL, -(nHat * n0) << ELL);
// Commit to p.
const mu = randBetween(BigInt(1) << ELL, BigInt(-1) << ELL);
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 mu should be
randBetween(BigInt(nHat) << ELL, BigInt(-nHat) << ELL) according to https://eprint.iacr.org/2020/492.pdf B.4

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

const P = (modPow(s, p, nHat) * modPow(t, mu, nHat)) % nHat;
// Commit to q.
const nu = randBetween(BigInt(1) << ELL, BigInt(-1) << ELL);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as mu

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

const Q = (modPow(s, q, nHat) * modPow(t, nu, nHat)) % nHat;
// Commit to alpha.
const x = randBetween(BigInt(1) << (ELL + EPSILON), BigInt(-1) << (ELL + EPSILON));
Copy link
Contributor

Choose a reason for hiding this comment

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

1 and -1 should be nHat and -nHat respectively.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

const A = (modPow(s, alpha, nHat) * modPow(t, x, nHat)) % nHat;
// Commit to beta.
const y = randBetween(BigInt(1) << (ELL + EPSILON), BigInt(-1) << (ELL + EPSILON));
Copy link
Contributor

Choose a reason for hiding this comment

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

1 and -1 should be nHat and -nHat respectively.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

const B = (modPow(s, beta, nHat) * modPow(t, y, nHat)) % nHat;
// Commit to Q and alpha.
const r = randBetween((nHat * n0) << (ELL + EPSILON), -(nHat * n0) << (ELL + EPSILON));
const T = (modPow(Q, alpha, nHat) * modPow(t, r, nHat)) % nHat;

const rhoHat = rho - nu * p;
const z1 = alpha + e * p;
const z2 = beta + e * q;
const w1 = x + e * mu;
const w2 = y + e * nu;
const v = r + e * rhoHat;

return { P, Q, A, B, T, rho, z1, z2, w1, w2, v };
}

/**
* Verify that $n0$ is not the product of any small factors.
* @param n0 - a modulus that is the product of $p$ and $q$.
* @param w - a random number with the same bitLength as $n0$, that satisfies the Jacobi of w is -1 wrt $n0$.
* @param nHat - a safe bi-prime, such as that returned from rangeProof.generateNTilde.
* @param s - security parameters for $nHat$ such as the $h1$ value returned from rangeProof.generateNTilde.
* @param t - security parameters for $nHat$ such as the $h2$ value returned from rangeProof.generateNTilde.
* @param proof - a proof generated by noSmallFactors.prove.
* @returns true if verification successful.
*/
export function verify(
n0: bigint,
w: bigint,
nHat: bigint,
s: bigint,
t: bigint,
proof: DeserializedNoSmallFactorsProof
): boolean {
const { P, Q, A, B, T, rho, z1, z2, w1, w2, v } = proof;
const e = generateE(n0, w);
const sqrtN0 = isqrt(n0);
const R = (modPow(s, n0, nHat) * modPow(t, rho, nHat)) % nHat;
if ((modPow(s, z1, nHat) * modPow(t, w1, nHat)) % nHat !== (A * modPow(P, e, nHat)) % nHat) {
throw new Error('Could not verify no small factors proof');
}
if ((modPow(s, z2, nHat) * modPow(t, w2, nHat)) % nHat !== (B * modPow(Q, e, nHat)) % nHat) {
throw new Error('Could not verify no small factors proof');
}
if ((modPow(Q, z1, nHat) * modPow(t, v, nHat)) % nHat !== (T * modPow(R, e, nHat)) % nHat) {
throw new Error('Could not verify no small factors proof');
}
if (z1 < -sqrtN0 << (ELL + EPSILON) || z1 > sqrtN0 << (ELL + EPSILON)) {
throw new Error('Could not verify no small factors proof');
}
if (z2 < -sqrtN0 << (ELL + EPSILON) || z2 > sqrtN0 << (ELL + EPSILON)) {
throw new Error('Could not verify no small factors proof');
}
return true;
}
149 changes: 149 additions & 0 deletions modules/sdk-lib-mpc/src/tss/ecdsa/paillierBlumProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { createHash } from 'crypto';
import { bitLength, randBits, isProbablyPrime } from 'bigint-crypto-utils';
import { gcd, modInv, modPow } from 'bigint-mod-arith';
import { bigIntFromBufferBE, bigIntToBufferBE } from '../../util';
import { DeserializedPaillierBlumProof } from './types';

// Security parameter.
const m = 80;

/**
* Generate psuedo-random quadratic residue for (N, w, i).
* @param N - the prime number to verify is a product of two large primes.
* @param w - a random number with the same bitLength as N, that satisfies the Jacobi of w is -1 wrt N.
* @returns {bigint[]} - set of challenges for N
*/
function generateY(N, w): bigint[] {
const NBuf = bigIntToBufferBE(N);
const wBuf = bigIntToBufferBE(w, NBuf.length);
let counter = 0;
return Array(m)
.fill(null)
.map((_) => {
while (true) {
let h = bigIntFromBufferBE(
// TypeScript doesn't like us using `outputLength` in the transform options,
// but it is required for shake256.
createHash('shake256', { outputLength: Math.floor((bitLength(N) + 7) / 8) })
.update(Buffer.from([counter++]))
.update('$')
.update(NBuf)
.update('$')
.update(wBuf)
.update('$')
.digest()
);
h = (h * h) % N;
if (gcd(h, N) === BigInt(1)) {
return h;
Copy link
Contributor

@zhongxishen zhongxishen Oct 3, 2023

Choose a reason for hiding this comment

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

The sampling of y here deviates from https://eprint.iacr.org/2020/492.pdf section 4.3 and the non-interactive version at https://www.zkdocs.com/docs/zkdocs/zero-knowledge-protocols/product-primes/paillier_blum_modulus/#non-interactive-protocol

Here it seems to be sampling y so that it will be the same as y' by implicitly making a = 0, b = 0 (and thus not including a, b in the proof). This will make only certain co-primes of N be picked as the challenge y, violating generateY() being a random oracle.

Copy link
Contributor

@zhongxishen zhongxishen Oct 3, 2023

Choose a reason for hiding this comment

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

Also, for y' (and therefore y here) to be suitable so that its 4th root is well defined, it needs to be a quadratic residue modulo N. Here by making y = h^2 and y (mod N) = 1, y is indeed a quadratic residue N but only for the cases where remainder is 1. Other quadratic residues are missed where x^2 === y (mod N) is other values (e.g. 4, 9, etc.). This further limits the sampling domain of the challenge value.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could refer to Fireblocks' implementation https://github.com/fireblocks/mpc-lib/blob/main/src/common/crypto/paillier/paillier_zkp.c#L648

Basically, due to p, q === 3 mod 4 and jacobi(w, N) === -1, it's guaranteed one of (0, 0), (0, 1), (1, 0), (1,1) for (a, b) will make y' a quadratic residue modulo N.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

}
}
});
}

// https://en.wikipedia.org/wiki/Jacobi_symbol#Implementation_in_C++
function jacobi(a, n): bigint {
// a/n is represented as (a,n)
if (n <= BigInt(0)) {
throw new Error('n must greater than 0');
}
if (n % BigInt(2) !== BigInt(1)) {
throw new Error('n must be odd');
}
// step 1
a = a % n;
let t = BigInt(1);
let r;
// step 3
while (a !== BigInt(0)) {
// step 2
while (a % BigInt(2) === BigInt(0)) {
a /= BigInt(2);
r = n % BigInt(8);
if (r === BigInt(3) || r === BigInt(5)) {
t = -t;
}
}
// step 4
r = n;
n = a;
a = r;
if (a % BigInt(4) === BigInt(3) && n % BigInt(4) === BigInt(3)) {
t = -t;
}
a = a % n;
}
if (n === BigInt(1)) {
return t;
}
return BigInt(0);
}

/**
* Prove that a modulus (p*q) is the product of two large safe primes (p and q).
* @param {bigint} p The larger prime factor of the modulus
* @param {bigint} q The smaller prime factor of the modulus.
* @returns {DeserializedPaillierBlumProof} The proof that the modulus is the product of two large primes.
*/
export async function prove(p: bigint, q: bigint): Promise<DeserializedPaillierBlumProof> {
// Prover selects random w with Jacobi symbol 1 wrt N.
const N = p * q;
const l = (p - BigInt(1)) * (q - BigInt(1));
const d = modInv(N, l);
let w;
while (true) {
w = bigIntFromBufferBE(Buffer.from(await randBits(bitLength(N))));
if (jacobi(w, N) === BigInt(-1)) {
break;
}
}
// This is calculating the inverse of the function y^4 mod N,
// i.e.y ^ (1 / 4), where N = pq is a blum integer using HOC - Fact 2.160
// from cacr.uwaterloo.ca / hac / about / chap2.pdf
// Prover generates y_i.
const y = generateY(N, w);
// Prover calculates z_i = y_i ^ d mod N
const z = y.map((y_i) => modPow(y_i, d, N));
// Prover calculates x_i = y_i ^ 1/4 mod N using [HOC - Fact 2.160]
const e = ((l + BigInt(4)) / BigInt(8)) ** BigInt(2);
const x = y.map((y_i) => modPow(y_i, e, N));
return { w, x, z };
}

/**
* Verify that N is the product of two large primes.
* @param {bigint} N The prime number being verified.
* @param {DeserializedPaillierBlumProof} The proof to verify N is a product of two large primes.
* @returns {boolean} True if N is a product of two large primes, and false otherwise.
*/
export async function verify(N: bigint, { w, x, z }: DeserializedPaillierBlumProof): Promise<boolean> {
// Verifier checks N > 1.
if (N <= 1) {
throw new Error('N must be greater than 1');
}
// Verifier checks N is odd.
if (N % BigInt(2) !== BigInt(1)) {
throw new Error('N must be an odd number');
}
// Verifier checks N is not prime.
if (await isProbablyPrime(N, 24)) {
throw new Error('N must be a composite number');
}
// Verifier checks that the Jacobi symbol for w is 1 wrt N.
if (jacobi(w, N) !== BigInt(-1)) {
throw new Error('Jacobi symbol of w must be -1 wrt to N');
}
// Verifier generates y_i.
const y = generateY(N, w);
for (let i = 0; i < m; i++) {
// Verifier checks z_i ^ N mod N == y_i.
if (modPow(z[i], N, N) !== y[i]) {
throw new Error(`Paillier verification of z[${i}] failed`);
}
// Verifier checks x_i ^ 4 mod N == y_i.
if (modPow(x[i], 4, N) !== y[i]) {
throw new Error(`Paillier verification of x[${i}] failed`);
}
}
return true;
}
Loading
Loading