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

Implement ElligatorSwift #107

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
128 changes: 127 additions & 1 deletion src/secp256k1.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { sha256 } from '@noble/hashes/sha256';
import { randomBytes } from '@noble/hashes/utils';
import { Field, mod, pow2 } from './abstract/modular.js';
import { Field, mod, pow2, FpIsSquare } from './abstract/modular.js';
import { ProjPointType as PointType, mapToCurveSimpleSWU } from './abstract/weierstrass.js';
import type { Hex, PrivKey } from './abstract/utils.js';
import { bytesToNumberBE, concatBytes, ensureBytes, numberToBytesBE } from './abstract/utils.js';
Expand Down Expand Up @@ -272,3 +272,129 @@ const htf = /* @__PURE__ */ (() =>
))();
export const hashToCurve = /* @__PURE__ */ (() => htf.hashToCurve)();
export const encodeToCurve = /* @__PURE__ */ (() => htf.encodeToCurve)();

// ElligatorSwift: Schnorr-like x-only ECDH with public keys indistinguishable
// from uniformly random bytes.
// https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki,
// https://github.com/bitcoin/bitcoin/blob/master/src/secp256k1/doc/ellswift.md
// SwiftEC: Shallue-van de Woestijne Indifferentiable Function to Elliptic Curves.
// https://eprint.iacr.org/2022/759.pdf
//
// Curve25519 & P-521 are incompatible with SwiftEC. Differences from SwiftEC:
// - undefined inputs are remapped
// - y-parity is encoded in u/t values
const MINUS_3_SQRT = Fp.sqrt(Fp.create(BigInt(-3)));
const _3n = BigInt(3);
const _4n = BigInt(4);
const _7n = BigInt(7);
const isSquare = FpIsSquare(Fp);
const isValidX = (x: bigint) => isSquare(Fp.add(Fp.mul(Fp.mul(x, x), x), _7n));
const trySqrt = (x: bigint): bigint | void => {
try {
return Fp.sqrt(x);
} catch (_e) {}
};

export const elligatorSwift = /* @__PURE__ */ {
// (internal stuff, exported for tests only): decode(u, _inv(x, u)) = x
_inv: (x: bigint, u: bigint, ellCase: number): bigint | void => {
if (!Number.isSafeInteger(ellCase) || ellCase < 0 || ellCase > 7)
throw new Error(`elligatorSwift._inv: wrong case=${ellCase}`);
let v: bigint, s: bigint;
// Most rejections happens in 3 condition (in comments, ~33% each)
const u2 = Fp.mul(u, u); // u**2
const u3 = Fp.mul(u2, u); // u**3
if ((ellCase & 2) === 0) {
if (isValidX(Fp.sub(Fp.neg(x), u))) return; // [1 condition]
v = x;
s = Fp.div(Fp.neg(Fp.add(u3, _7n)), Fp.add(Fp.add(u2, Fp.mul(u, v)), Fp.mul(v, v))); // = -(u**3 + 7) / (u**2 + u*v + v**2)
} else {
s = Fp.sub(x, u); // x - u
if (Fp.is0(s)) return;
const t0 = Fp.add(u3, _7n); // (u**3 + 7)
const t1 = Fp.mul(Fp.mul(_3n, s), u2); // 3 * s * u**2
// r = (-s * (4 * (u**3 + 7) + 3 * s * u**2)).sqrt()
const r = trySqrt(Fp.mul(Fp.neg(s), Fp.add(Fp.mul(_4n, t0), t1)));
if (r === undefined) return; // [2 condition]
if (ellCase & 1 && Fp.is0(r)) return;
v = Fp.div(Fp.add(Fp.neg(u), Fp.div(r, s)), _2n); // v = (-u + r / s) / 2
}
const w = trySqrt(s);
if (w === undefined) return; // [3 condition]
const last = ellCase & 5; // ellCase = 0..8, last = 0,1,4,5
const t0 = last & 1 ? Fp.add(_1n, MINUS_3_SQRT) : Fp.sub(_1n, MINUS_3_SQRT);
const w0 = last === 0 || last === 5 ? Fp.neg(w) : w; // -w | w
// w0 * (u * t0 / 2 + v)
return Fp.mul(w0, Fp.add(Fp.div(Fp.mul(u, t0), _2n), v));
},
// Encode public key (point or x coordinate bigint) into 64-byte pseudorandom encoding
encode: (x: bigint | PointType<bigint>): Uint8Array => {
if (x instanceof secp256k1.ProjectivePoint) x = x.x;
if (typeof x !== 'bigint') {
throw new Error(
'elligatorSwift.encode: wrong public key. Should be Projective point or x coordinate (bigint)'
);
}
// 200k test cycles per keygen: avg=4 max=48
// seems too much, but same as for reference implementation
while (true) {
// random scalar 1..Fp.ORDER
const u = Fp.create(Fp.fromBytes(secp256k1.utils.randomPrivateKey()));
const ellCase = randomBytes(1)[0] & 7; // [0..8)
const t = elligatorSwift._inv(x, u, ellCase);
if (!t) continue;
return concatBytes(numberToBytesBE(u, 32), numberToBytesBE(t, 32));
}
},
// Decode elligatorSwift point to xonly
decode: (data: Hex): Uint8Array => {
const _data = ensureBytes('data', data, 64);
let u = Fp.create(Fp.fromBytes(_data.subarray(0, 32)));
let t = Fp.create(Fp.fromBytes(_data.subarray(32, 64)));
if (Fp.is0(u)) u = Fp.create(_1n);
if (Fp.is0(t)) t = Fp.create(_1n);
const u3 = Fp.mul(Fp.mul(u, u), u); // u**3
const u3plus7 = Fp.add(u3, _7n);
// u**3 + t**2 + 7 == 0 -> t = 2 * t
if (Fp.is0(Fp.add(u3plus7, Fp.mul(t, t)))) t = Fp.add(t, t);
// X = (u**3 + 7 - t**2) / (2 * t)
const x = Fp.div(Fp.sub(u3plus7, Fp.mul(t, t)), Fp.add(t, t));
// Y = (X + t) / (MINUS_3_SQRT * u);
const y = Fp.div(Fp.add(x, t), Fp.mul(MINUS_3_SQRT, u));
// try different cases
let res = Fp.add(u, Fp.mul(Fp.mul(y, y), _4n)); // u + 4 * Y ** 2,
if (isValidX(res)) return numberToBytesBE(res, 32);
res = Fp.div(Fp.sub(Fp.div(Fp.neg(x), y), u), _2n); // (-X / Y - u) / 2
if (isValidX(res)) return numberToBytesBE(res, 32);
res = Fp.div(Fp.sub(Fp.div(x, y), u), _2n); // (X / Y - u) / 2
if (isValidX(res)) return numberToBytesBE(res, 32);
throw new Error('elligatorSwift: cannot decode public key');
},
// Generate pair (public key, secret key)
keygen: () => {
const privateKey = secp256k1.utils.randomPrivateKey();
const publicKey = elligatorSwift.encode(Point.fromPrivateKey(privateKey));
return { privateKey, publicKey };
},
// Generates shared secret between a pub key and a priv key
getSharedSecret: (privateKeyA: Hex, publicKeyB: Hex) => {
const pub = elligatorSwift.decode(publicKeyB);
const priv = ensureBytes('privKey', privateKeyA, 32);
const point = lift_x(Fp.fromBytes(pub));
const d = bytesToNumberBE(priv);
return numberToBytesBE(point.multiply(d).x, 32);
},
// BIP324 shared secret
getSharedSecretBip324: (
privateKeyOurs: Hex,
publicKeyTheirs: Hex,
publicKeyOurs: Hex,
initiating: boolean
) => {
const ours = ensureBytes('publicKeyOurs', publicKeyOurs);
const theirs = ensureBytes('publicKeyTheirs', publicKeyTheirs);
const ecdhPoint = elligatorSwift.getSharedSecret(privateKeyOurs, theirs);
const pubs = initiating ? [ours, theirs] : [theirs, ours];
return taggedHash('bip324_ellswift_xonly_ecdh', ...pubs, ecdhPoint);
},
};
1 change: 1 addition & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import './ed25519.test.js';
import './ed25519-addons.test.js';
import './secp256k1.test.js';
import './secp256k1-schnorr.test.js';
import './secp256k1-bip-0324.test.js';
import './jubjub.test.js';
import './hash-to-curve.test.js';
import './poseidon.test.js';
Expand Down
122 changes: 122 additions & 0 deletions test/secp256k1-bip-0324.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { deepStrictEqual } from 'assert';
import { should, describe } from 'micro-should';
import * as fs from 'fs';
import {
hexToBytes,
hexToNumber,
concatBytes,
bytesToHex as toHex,
} from '../esm/abstract/utils.js';
// Generic tests for all curves in package
import { secp256k1, elligatorSwift } from '../esm/secp256k1.js';
// ESM is broken.
import { dirname } from 'path';
import { fileURLToPath } from 'url';
export const __dirname = dirname(fileURLToPath(import.meta.url));

// https://eprint.iacr.org/2022/759

const parseCSV = (path) => {
const data = fs.readFileSync(`${__dirname}/vectors/secp256k1/${path}`, 'utf8');
const lines = data.split('\n').filter((i) => !!i);
const rows = lines.map((i) => i.trim().split(','));
const lengths = new Set(rows.map((i) => i.length));
if (lengths.size !== 1) throw new Error('wrong dimensions');
if (rows.length < 2) throw new Error('wrong rows length');
const [head, ...rest] = rows;
return rest.map((row) => Object.fromEntries(row.map((cell, j) => [head[j], cell])));
};

describe('ElligatorSwift', () => {
should('packet_encoding_test_vectors', () => {
for (const t of parseCSV('bip-0324/packet_encoding_test_vectors.csv')) {
const inPriv = hexToNumber(t['in_priv_ours']);
const pubX = secp256k1.ProjectivePoint.BASE.multiply(inPriv)
.x.toString(16)
.padStart(2 * 32, '0');
deepStrictEqual(pubX, t['mid_x_ours']);

const bytesOurs = hexToBytes(t['in_ellswift_ours']);
const decoded = elligatorSwift.decode(bytesOurs);
deepStrictEqual(toHex(decoded), t['mid_x_ours']);

const bytesTheirs = hexToBytes(t['in_ellswift_theirs']);
deepStrictEqual(toHex(elligatorSwift.decode(bytesTheirs)), t['mid_x_theirs']);

const xShared = elligatorSwift.getSharedSecret(t['in_priv_ours'], bytesTheirs);
deepStrictEqual(toHex(xShared), t['mid_x_shared']);

const sharedSecret = elligatorSwift.getSharedSecretBip324(
t['in_priv_ours'],
t['in_ellswift_theirs'],
t['in_ellswift_ours'],
t['in_initiating'] === '1'
);
deepStrictEqual(toHex(sharedSecret), t['mid_shared_secret']);
}
});

should('xswiftec_inv_test_vectors', () => {
for (const t of parseCSV('bip-0324/xswiftec_inv_test_vectors.csv')) {
const Fp = secp256k1.CURVE.Fp;
const u = Fp.create(Fp.fromBytes(hexToBytes(t['u'])));
const x = Fp.create(Fp.fromBytes(hexToBytes(t['x'])));
for (let c = 0; c < 8; c++) {
const name = `case${c}_t`;
const ret = elligatorSwift._inv(x, u, c);
if (!ret) deepStrictEqual(t[name], '', 'empty case');
else {
deepStrictEqual(toHex(Fp.toBytes(ret)), t[name], 'real case');
deepStrictEqual(
elligatorSwift.decode(concatBytes(Fp.toBytes(u), Fp.toBytes(ret))),
Fp.toBytes(x)
);
}
}
}
});

should('ellswift_decode_test_vectors', () => {
for (const t of parseCSV('bip-0324/ellswift_decode_test_vectors.csv')) {
deepStrictEqual(toHex(elligatorSwift.decode(t['ellswift'])), t['x']);
}
});
should('Example', () => {
// random, so test more.
for (let i = 0; i < 100; i++) {
const alice = elligatorSwift.keygen();
const bob = elligatorSwift.keygen();
// ECDH
const sharedAlice = elligatorSwift.getSharedSecret(alice.privateKey, bob.publicKey);
const sharedBob = elligatorSwift.getSharedSecret(bob.privateKey, alice.publicKey);
deepStrictEqual(sharedAlice, sharedBob);
// ECDH BIP324
const sharedAlice2 = elligatorSwift.getSharedSecretBip324(
alice.privateKey,
bob.publicKey,
alice.publicKey,
true
);
const sharedBob2 = elligatorSwift.getSharedSecretBip324(
bob.privateKey,
alice.publicKey,
bob.publicKey,
false
);
deepStrictEqual(sharedAlice2, sharedBob2);
// pubKey decoding
for (const k of [alice, bob]) {
deepStrictEqual(
toHex(elligatorSwift.decode(k.publicKey)),
toHex(secp256k1.getPublicKey(k.privateKey, true).subarray(1))
);
}
}
});
});

// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}
Loading
Loading