Skip to content

Commit

Permalink
Implement ElligatorSwift
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Jan 14, 2024
1 parent 0a66339 commit 943edbc
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 1 deletion.
123 changes: 122 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,124 @@ 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

0 comments on commit 943edbc

Please sign in to comment.