weierstrass: rename normPrivKey util. tests: prepare for unification w old noble pkg

This commit is contained in:
Paul Miller 2023-02-09 19:26:20 +00:00
parent debb9d9709
commit 1b6071cabd
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
5 changed files with 92 additions and 55 deletions

@ -636,7 +636,7 @@ export type CurveFn = {
ProjectivePoint: ProjConstructor<bigint>; ProjectivePoint: ProjConstructor<bigint>;
Signature: SignatureConstructor; Signature: SignatureConstructor;
utils: { utils: {
_normalizePrivateKey: (key: PrivKey) => bigint; normPrivateKeyToScalar: (key: PrivKey) => bigint;
isValidPrivateKey(privateKey: PrivKey): boolean; isValidPrivateKey(privateKey: PrivKey): boolean;
hashToPrivateKey: (hash: Hex) => Uint8Array; hashToPrivateKey: (hash: Hex) => Uint8Array;
randomPrivateKey: () => Uint8Array; randomPrivateKey: () => Uint8Array;
@ -858,7 +858,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
return false; return false;
} }
}, },
_normalizePrivateKey: normalizePrivateKey, normPrivateKeyToScalar: normalizePrivateKey,
/** /**
* Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes. * Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes.
@ -993,7 +993,16 @@ export function weierstrass(curveDef: CurveType): CurveFn {
const q = Point.BASE.multiply(k).toAffine(); // q = Gk const q = Point.BASE.multiply(k).toAffine(); // q = Gk
const r = modN(q.x); // r = q.x mod n const r = modN(q.x); // r = q.x mod n
if (r === _0n) return; if (r === _0n) return;
const s = modN(ik * modN(m + modN(d * r))); // s = k^-1(m + rd) mod n // X blinding according to https://tches.iacr.org/index.php/TCHES/article/view/7337/6509
// b * m + b * r * d ∈ [0,q1] exposed via side-channel, but d (private scalar) is not.
// NOTE: there is still probable some leak in multiplication, since it is not constant-time
const b = ut.bytesToNumberBE(utils.randomPrivateKey()); // random scalar, b ∈ [1,q1]
const bi = invN(b); // b^-1
const bdr = modN(b * d * r); // b * d * r
const bm = modN(b * m); // b * m
const mrx = modN(bi * modN(bdr + bm)); // b^-1(bm + bdr) -> m + rd
const s = modN(ik * mrx); // s = k^-1(m + rd) mod n
if (s === _0n) return; if (s === _0n) return;
let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n); // recovery bit (2 or 3, when q.x > n) let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n); // recovery bit (2 or 3, when q.x > n)
let normS = s; let normS = s;

@ -6,6 +6,7 @@ import './nist.test.js';
import './ed448.test.js'; import './ed448.test.js';
import './ed25519.test.js'; import './ed25519.test.js';
import './secp256k1.test.js'; import './secp256k1.test.js';
import './secp256k1-schnorr.test.js';
import './stark/index.test.js'; import './stark/index.test.js';
import './jubjub.test.js'; import './jubjub.test.js';
import './bls12-381.test.js'; import './bls12-381.test.js';

@ -0,0 +1,34 @@
import { deepStrictEqual, throws } from 'assert';
import { readFileSync } from 'fs';
import { should, describe } from 'micro-should';
import { bytesToHex as hex } from '@noble/hashes/utils';
import { schnorr } from '../lib/esm/secp256k1.js';
const schCsv = readFileSync('./test/vectors/schnorr.csv', 'utf-8');
describe('schnorr.sign()', () => {
// index,secret key,public key,aux_rand,message,signature,verification result,comment
const vectors = schCsv
.split('\n')
.map((line) => line.split(','))
.slice(1, -1);
for (let vec of vectors) {
const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec;
should(`${comment || 'vector ' + index}`, () => {
if (sec) {
deepStrictEqual(hex(schnorr.getPublicKey(sec)), pub.toLowerCase());
const sig = schnorr.sign(msg, sec, rnd);
deepStrictEqual(hex(sig), expSig.toLowerCase());
deepStrictEqual(schnorr.verify(sig, msg, pub), true);
} else {
const passed = schnorr.verify(expSig, msg, pub);
deepStrictEqual(passed, passes === 'TRUE');
}
});
}
});
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}

14
test/secp256k1.helpers.js Normal file

@ -0,0 +1,14 @@
// @ts-ignore
export { secp256k1 as secp } from '../lib/esm/secp256k1.js';
import { secp256k1 as _secp } from '../lib/esm/secp256k1.js';
export { bytesToNumberBE, numberToBytesBE } from '../lib/esm/abstract/utils.js';
export { mod } from '../lib/esm/abstract/modular.js';
export const sigFromDER = (der) => {
return _secp.Signature.fromDER(der);
};
export const sigToDER = (sig) => sig.toDERHex();
export const selectHash = (secp) => secp.CURVE.hash;
export const normVerifySig = (s) => _secp.Signature.fromDER(s);
// export const bytesToNumberBE = secp256k1.utils.bytesToNumberBE;
// export const numberToBytesBE = secp256k1.utils.numberToBytesBE;
// export const mod = mod_;

@ -1,22 +1,21 @@
import { hexToBytes, bytesToHex as hex } from '@noble/hashes/utils';
import { deepStrictEqual, throws } from 'assert';
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import { secp256k1, schnorr } from '../lib/esm/secp256k1.js';
import { Fp } from '../lib/esm/abstract/modular.js';
import { bytesToNumberBE, ensureBytes, numberToBytesBE } from '../lib/esm/abstract/utils.js';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { should, describe } from 'micro-should';
// prettier-ignore
import {
secp, sigFromDER, sigToDER, selectHash, normVerifySig, mod, bytesToNumberBE, numberToBytesBE
} from './secp256k1.helpers.js';
import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' }; import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' };
import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' }; import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' };
import { default as privates } from './vectors/privates.json' assert { type: 'json' }; import { default as privates } from './vectors/privates.json' assert { type: 'json' };
import { default as points } from './vectors/points.json' assert { type: 'json' }; import { default as points } from './vectors/points.json' assert { type: 'json' };
import { default as wp } from './vectors/wychenproof.json' assert { type: 'json' }; import { default as wp } from './vectors/wychenproof.json' assert { type: 'json' };
import { should, describe } from 'micro-should';
import { deepStrictEqual, throws } from 'assert';
import { hexToBytes, bytesToHex } from '@noble/hashes/utils';
const hex = bytesToHex;
const secp = secp256k1;
const Point = secp.ProjectivePoint; const Point = secp.ProjectivePoint;
const privatesTxt = readFileSync('./test/vectors/privates-2.txt', 'utf-8'); const privatesTxt = readFileSync('./test/vectors/privates-2.txt', 'utf-8');
const schCsv = readFileSync('./test/vectors/schnorr.csv', 'utf-8');
const FC_BIGINT = fc.bigInt(1n + 1n, secp.CURVE.n - 1n); const FC_BIGINT = fc.bigInt(1n + 1n, secp.CURVE.n - 1n);
// prettier-ignore // prettier-ignore
@ -193,7 +192,7 @@ describe('secp256k1', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (r, s) => { fc.property(FC_BIGINT, FC_BIGINT, (r, s) => {
const sig = new secp.Signature(r, s); const sig = new secp.Signature(r, s);
deepStrictEqual(secp.Signature.fromDER(sig.toDERHex()), sig); deepStrictEqual(sigFromDER(sigToDER(sig)), sig);
}) })
); );
}); });
@ -241,9 +240,9 @@ describe('secp256k1', () => {
); );
for (const [msg, exp] of CASES) { for (const [msg, exp] of CASES) {
const res = secp.sign(msg, privKey, { extraEntropy: undefined }); const res = secp.sign(msg, privKey, { extraEntropy: undefined });
deepStrictEqual(res.toDERHex(), exp); deepStrictEqual(sigToDER(res), exp);
const rs = secp.Signature.fromDER(res.toDERHex()).toCompactHex(); const rs = sigFromDER(sigToDER(res)).toCompactHex();
deepStrictEqual(secp.Signature.fromCompact(rs).toDERHex(), exp); deepStrictEqual(sigToDER(secp.Signature.fromCompact(rs)), exp);
} }
}); });
should('handle {extraData} option', () => { should('handle {extraData} option', () => {
@ -342,7 +341,7 @@ describe('secp256k1', () => {
const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n; const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n;
const pub = new Point(x, y, 1n).toRawBytes(); const pub = new Point(x, y, 1n).toRawBytes();
const sig = new secp.Signature(r, s); const sig = new secp.Signature(r, s);
deepStrictEqual(secp.verify(sig, msg, pub, { strict: false }), true); deepStrictEqual(secp.verify(sig, msg, pub, { lowS: false }), true);
}); });
should('not verify invalid deterministic signatures with RFC 6979', () => { should('not verify invalid deterministic signatures with RFC 6979', () => {
for (const vector of ecdsa.invalid.verify) { for (const vector of ecdsa.invalid.verify) {
@ -351,29 +350,6 @@ describe('secp256k1', () => {
} }
}); });
}); });
describe('schnorr.sign()', () => {
// index,secret key,public key,aux_rand,message,signature,verification result,comment
const vectors = schCsv
.split('\n')
.map((line) => line.split(','))
.slice(1, -1);
for (let vec of vectors) {
const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec;
should(`${comment || 'vector ' + index}`, () => {
if (sec) {
deepStrictEqual(hex(schnorr.getPublicKey(sec)), pub.toLowerCase());
const sig = schnorr.sign(msg, sec, rnd);
deepStrictEqual(hex(sig), expSig.toLowerCase());
deepStrictEqual(schnorr.verify(sig, msg, pub), true);
} else {
const passed = schnorr.verify(expSig, msg, pub);
deepStrictEqual(passed, passes === 'TRUE');
}
});
}
});
describe('recoverPublicKey()', () => { describe('recoverPublicKey()', () => {
should('recover public key from recovery bit', () => { should('recover public key from recovery bit', () => {
const message = '00000000000000000000000000000000000000000000000000000000deadbeef'; const message = '00000000000000000000000000000000000000000000000000000000deadbeef';
@ -404,7 +380,7 @@ describe('secp256k1', () => {
should('handle RFC 6979 vectors', () => { should('handle RFC 6979 vectors', () => {
for (const vector of ecdsa.valid) { for (const vector of ecdsa.valid) {
let usig = secp.sign(vector.m, vector.d); let usig = secp.sign(vector.m, vector.d);
let sig = usig.toDERHex(); let sig = sigToDER(usig);
const vpub = secp.getPublicKey(vector.d); const vpub = secp.getPublicKey(vector.d);
const recovered = usig.recoverPublicKey(vector.m); const recovered = usig.recoverPublicKey(vector.m);
deepStrictEqual(recovered.toHex(), hex(vpub)); deepStrictEqual(recovered.toHex(), hex(vpub));
@ -459,24 +435,25 @@ describe('secp256k1', () => {
}); });
describe('tweak utilities (legacy)', () => { describe('tweak utilities (legacy)', () => {
const Fn = Fp(secp.CURVE.n); const normal = secp.utils.normPrivateKeyToScalar;
const normal = secp.utils._normalizePrivateKey;
const tweakUtils = { const tweakUtils = {
privateAdd: (privateKey, tweak) => { privateAdd: (privateKey, tweak) => {
return numberToBytesBE(Fn.add(normal(privateKey), normal(tweak)), 32); return numberToBytesBE(mod(normal(privateKey) + normal(tweak), secp.CURVE.n), 32);
}, },
privateNegate: (privateKey) => { privateNegate: (privateKey) => {
return numberToBytesBE(Fn.neg(normal(privateKey)), 32); return numberToBytesBE(mod(-normal(privateKey), secp.CURVE.n), 32);
}, },
pointAddScalar: (p, tweak, isCompressed) => { pointAddScalar: (p, tweak, isCompressed) => {
// Will throw if tweaked point is at infinity const tweaked = Point.fromHex(p).add(Point.fromPrivateKey(tweak));
return Point.fromHex(p).add(Point.fromPrivateKey(tweak)).toRawBytes(isCompressed); if (tweaked.equals(Point.ZERO)) throw new Error('Tweaked point at infinity');
return tweaked.toRawBytes(isCompressed);
}, },
pointMultiply: (p, tweak, isCompressed) => { pointMultiply: (p, tweak, isCompressed) => {
const t = bytesToNumberBE(ensureBytes(tweak)); if (typeof tweak === 'string') tweak = hexToBytes(tweak);
const t = bytesToNumberBE(tweak);
return Point.fromHex(p).multiply(t).toRawBytes(isCompressed); return Point.fromHex(p).multiply(t).toRawBytes(isCompressed);
}, },
}; };
@ -484,20 +461,20 @@ describe('secp256k1', () => {
should('privateAdd()', () => { should('privateAdd()', () => {
for (const vector of privates.valid.add) { for (const vector of privates.valid.add) {
const { a, b, expected } = vector; const { a, b, expected } = vector;
deepStrictEqual(bytesToHex(tweakUtils.privateAdd(a, b)), expected); deepStrictEqual(hex(tweakUtils.privateAdd(a, b)), expected);
} }
}); });
should('privateNegate()', () => { should('privateNegate()', () => {
for (const vector of privates.valid.negate) { for (const vector of privates.valid.negate) {
const { a, expected } = vector; const { a, expected } = vector;
deepStrictEqual(bytesToHex(tweakUtils.privateNegate(a)), expected); deepStrictEqual(hex(tweakUtils.privateNegate(a)), expected);
} }
}); });
should('pointAddScalar()', () => { should('pointAddScalar()', () => {
for (const vector of points.valid.pointAddScalar) { for (const vector of points.valid.pointAddScalar) {
const { description, P, d, expected } = vector; const { description, P, d, expected } = vector;
const compressed = !!expected && expected.length === 66; // compressed === 33 bytes const compressed = !!expected && expected.length === 66; // compressed === 33 bytes
deepStrictEqual(bytesToHex(tweakUtils.pointAddScalar(P, d, compressed)), expected); deepStrictEqual(hex(tweakUtils.pointAddScalar(P, d, compressed)), expected);
} }
}); });
should('pointAddScalar() invalid', () => { should('pointAddScalar() invalid', () => {
@ -509,7 +486,7 @@ describe('secp256k1', () => {
should('pointMultiply()', () => { should('pointMultiply()', () => {
for (const vector of points.valid.pointMultiply) { for (const vector of points.valid.pointMultiply) {
const { P, d, expected } = vector; const { P, d, expected } = vector;
deepStrictEqual(bytesToHex(tweakUtils.pointMultiply(P, d, true)), expected); deepStrictEqual(hex(tweakUtils.pointMultiply(P, d, true)), expected);
} }
}); });
should('pointMultiply() invalid', () => { should('pointMultiply() invalid', () => {
@ -525,10 +502,12 @@ describe('secp256k1', () => {
// const pubKey = Point.fromHex().toRawBytes(); // const pubKey = Point.fromHex().toRawBytes();
const pubKey = group.key.uncompressed; const pubKey = group.key.uncompressed;
for (let test of group.tests) { for (let test of group.tests) {
const m = secp.CURVE.hash(hexToBytes(test.msg)); const h = selectHash(secp);
const m = h(hexToBytes(test.msg));
if (test.result === 'valid' || test.result === 'acceptable') { if (test.result === 'valid' || test.result === 'acceptable') {
const verified = secp.verify(test.sig, m, pubKey); const verified = secp.verify(normVerifySig(test.sig), m, pubKey);
if (secp.Signature.fromDER(test.sig).hasHighS()) { if (sigFromDER(test.sig).hasHighS()) {
deepStrictEqual(verified, false); deepStrictEqual(verified, false);
} else { } else {
deepStrictEqual(verified, true); deepStrictEqual(verified, true);