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

Add BIP340 Schnorr signatures. #132

Open
wants to merge 1 commit 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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/*
30 changes: 28 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ declare const getPublicKey: (privKey: PrivKey, isCompressed?: boolean) => Bytes;
declare class Signature {
readonly r: bigint;
readonly s: bigint;
readonly recovery?: number | undefined;
constructor(r: bigint, s: bigint, recovery?: number | undefined);
readonly recovery?: number;
constructor(r: bigint, s: bigint, recovery?: number);
/** Create signature from 64b compact (r || s) representation. */
static fromCompact(hex: Hex): Signature;
assertValidity(): Signature;
Expand Down Expand Up @@ -144,6 +144,7 @@ declare const verify: (sig: Hex | SigLike, msgh: Hex, pub: Hex, opts?: OptV) =>
declare const getSharedSecret: (privA: Hex, pubB: Hex, isCompressed?: boolean) => Bytes;
/** Math, hex, byte helpers. Not in `utils` because utils share API with noble-curves. */
declare const etc: {
au8: (a: unknown, l?: number) => Bytes;
hexToBytes: (hex: string) => Bytes;
bytesToHex: (bytes: Bytes) => string;
concatBytes: (...arrs: Bytes[]) => Bytes;
Expand All @@ -154,6 +155,8 @@ declare const etc: {
hmacSha256Async: (key: Bytes, ...msgs: Bytes[]) => Promise<Bytes>;
hmacSha256Sync: HmacFnSync;
hashToPrivateKey: (hash: Hex) => Bytes;
sqrt: (n: bigint) => bigint;
err: (m?: string) => never;
randomBytes: (len?: number) => Bytes;
};
/** Curve-specific utilities for private keys. */
Expand All @@ -165,3 +168,26 @@ declare const utils: {
};
export { CURVE, etc, getPublicKey, // Remove the export to easily use in REPL
getSharedSecret, Point as ProjectivePoint, sign, signAsync, Signature, utils, verify };
/**
* Schnorr public key is just `x` coordinate of Point as per BIP340.
*/
declare function getPublicKeySch(privateKey: Hex): Bytes;
/**
* Creates Schnorr signature as per BIP340. Verifies itself before returning anything.
* auxRand is optional and is not the sole source of k generation: bad CSPRNG won't be dangerous.
*/
declare function signSch(message: Bytes, privateKey: PrivKey, auxRand?: Bytes): Bytes;
declare function signAsyncSch(message: Bytes, privateKey: PrivKey, auxRand?: Bytes): Promise<Bytes>;
/**
* Verifies Schnorr signature.
* Will swallow errors & return false except for initial type validation of arguments.
*/
declare function verifySch(signature: Bytes, message: Bytes, publicKey: Bytes): boolean;
declare function verifyAsyncSch(signature: Bytes, message: Bytes, publicKey: Bytes): Promise<boolean>;
export declare const schnorr: {
getPublicKey: typeof getPublicKeySch;
sign: typeof signSch;
verify: typeof verifySch;
signAsync: typeof signAsyncSch;
verifyAsync: typeof verifyAsyncSch;
};
217 changes: 196 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ class Signature {
constructor(r, s, recovery) {
this.r = r;
this.s = s;
this.recovery = recovery;
if (recovery != null)
this.recovery = recovery;
this.assertValidity(); // recovery bit is optional when
Object.freeze(this);
} // constructed outside.
Expand Down Expand Up @@ -315,7 +316,6 @@ const bits2int_modN = (bytes) => {
const i2o = (num) => n2b(num); // int to octets
const cr = () => // We support: 1) browsers 2) node.js 19+ 3) deno, other envs with crypto
typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined;
let _hmacSync; // Can be redefined by use in utils; built-ins don't provide it
const optS = { lowS: true }; // opts for sign()
const optV = { lowS: true }; // standard opts for verify()
const prepSig = (msgh, priv, opts = optS) => {
Expand Down Expand Up @@ -388,10 +388,10 @@ function hmacDrbg(asynchronous) {
}
else {
const h = (...b) => {
const f = _hmacSync;
if (!f)
const fn = etc.hmacSha256Sync;
if (typeof fn !== 'function')
err('etc.hmacSha256Sync not set');
return f(k, v, ...b); // hmac(k)(v, ...values)
return fn(k, v, ...b); // hmac(k)(v, ...values)
};
const reseed = (seed = u8n()) => {
k = h(u8n([0x00]), seed); // k = hmac(k || v || 0x00 || seed)
Expand Down Expand Up @@ -478,7 +478,7 @@ const verify = (sig, msgh, pub, opts = optV) => {
const { r, s } = sig_;
if (lowS && high(s))
return false; // lowS bans sig.s >= CURVE.n/2
let R;
let R; // Actual verification code begins here
try {
const is = inv(s, N); // s^-1
const u1 = M(h * is, N); // u1 = hs^-1 mod n
Expand Down Expand Up @@ -511,8 +511,15 @@ const hashToPrivateKey = (hash) => {
const num = M(b2n(hash), N - 1n); // takes n+8 bytes
return n2b(num + 1n); // returns (hash mod n-1)+1
};
const crRandom = (len = 32) => {
const c = cr(); // Must be shimmed in node.js <= 18 to prevent error. See README.
if (!c || !c.getRandomValues)
err('crypto.getRandomValues must be defined');
return c.getRandomValues(u8n(len));
};
/** Math, hex, byte helpers. Not in `utils` because utils share API with noble-curves. */
const etc = {
au8: au8,
hexToBytes: h2b,
bytesToHex: b2h,
concatBytes: concatB,
Expand All @@ -521,21 +528,15 @@ const etc = {
mod: M,
invert: inv, // math utilities
hmacSha256Async: async (key, ...msgs) => {
const c = cr(); // async HMAC-SHA256, no sync built-in!
const s = c && c.subtle; // For React Native support, see README.
if (!s)
return err('etc.hmacSha256Async or crypto.subtle must be defined'); // Uses webcrypto built-in cryptography.
const s = subtle();
const k = await s.importKey('raw', key, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']);
return u8n(await s.sign('HMAC', k, concatB(...msgs)));
},
hmacSha256Sync: _hmacSync, // For TypeScript. Actual logic is below
hmacSha256Sync: undefined, // For TypeScript. Actual logic is below
hashToPrivateKey: hashToPrivateKey,
randomBytes: (len = 32) => {
const crypto = cr(); // Must be shimmed in node.js <= 18 to prevent error. See README.
if (!crypto || !crypto.getRandomValues)
err('crypto.getRandomValues must be defined');
return crypto.getRandomValues(u8n(len));
},
sqrt: sqrt,
err: err,
randomBytes: crRandom,
};
/** Curve-specific utilities for private keys. */
const utils = {
Expand All @@ -549,10 +550,10 @@ const utils = {
randomPrivateKey: () => hashToPrivateKey(etc.randomBytes(fLen + 16)), // FIPS 186 B.4.1.
precompute: (w = 8, p = G) => { p.multiply(3n); w; return p; }, // no-op
};
Object.defineProperties(etc, { hmacSha256Sync: {
configurable: false, get() { return _hmacSync; }, set(f) { if (!_hmacSync)
_hmacSync = f; },
} });
const subtle = () => {
const c = cr();
return c && c.subtle || err('crypto.subtle must be defined');
};
const W = 8; // Precomputes-related code. W = window size
const precompute = () => {
const points = []; // 10x sign(), 2x verify(). To achieve this,
Expand Down Expand Up @@ -601,3 +602,177 @@ const wNAF = (n) => {
}; // !! you can disable precomputes by commenting-out call of the wNAF() inside Point#mul()
export { CURVE, etc, getPublicKey, // Remove the export to easily use in REPL
getSharedSecret, Point as ProjectivePoint, sign, signAsync, Signature, utils, verify }; // envs like browser console
// import { type Bytes, type Hex, type PrivKey, CURVE, etc, utils as eutils, ProjectivePoint as Point } from './index.ts';
// const { p: P, n: N } = CURVE;
// const { mod: M, sqrt, concatBytes: concatB, numberToBytesBE: n2b, bytesToNumberBE: num, au8, err } = etc;
// const { normPrivateKeyToScalar: toPriv } = eutils;
// const G = Point.BASE;
// Schnorr signatures are superior to ECDSA from above. Below is Schnorr-specific BIP0340 code.
// /~https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
const etc_schnorr = {
sha256Async: async (...msgs) => {
return u8n(await subtle().digest("SHA-256", concatB(...msgs)));
},
sha256Sync: undefined,
randomBytes: crRandom,
};
const utf8 = (m) => Uint8Array.from(m, (c) => c.charCodeAt(0));
const taggedHash = (tag, ...messages) => {
const fn = etc_schnorr.sha256Sync;
if (typeof fn !== 'function')
return err('etc_schnorr.sha256Sync not set');
const tagH = fn(utf8(tag));
return fn(concatB(tagH, tagH, ...messages));
};
const taggedHashAsync = async (tag, ...messages) => {
const fn = etc_schnorr.sha256Async;
const tagH = await fn(utf8(tag));
return await fn(concatB(tagH, tagH, ...messages));
};
// ECDSA compact points are 33-byte. Schnorr is 32: we strip first byte 0x02 or 0x03
const pointToBytes = (point) => point.toRawBytes(true).slice(1);
const modP = (x) => M(x, P);
const modN = (x) => M(x, N);
const hasEvenY = (p) => (p.y & 1n) !== 1n;
// Calculate point, scalar and bytes
function schnorrGetExtPubKey(priv) {
let d_ = toPriv(priv); // same method executed in fromPrivateKey
let p = Point.fromPrivateKey(d_); // P = d'⋅G; 0 < d' < n check is done inside
const scalar = hasEvenY(p) ? d_ : modN(-d_);
return { scalar: scalar, bytes: pointToBytes(p) };
}
function inRange(num, max) {
return 1n <= num && num < max; // 1..num..max
}
/**
* lift_x from BIP340. Convert 32-byte x coordinate to elliptic curve point.
* @returns valid point checked for being on-curve
*/
function lift_x(x) {
if (!inRange(x, P))
err('expected x >= p'); // Fail if x ≥ p.
const xx = modP(x * x);
const c = modP(xx * x + BigInt(7)); // Let c = x³ + 7 mod p.
let y = sqrt(c); // Let y = c^(p+1)/4 mod p.
if (y % 2n !== 0n)
y = modP(-y); // Return the unique point P such that x(P) = x and
const p = new Point(x, y, 1n); // y(P) = y if y mod 2 = 0 or y(P) = p-y otherwise.
p.assertValidity();
return p;
}
const TG = {
cl: 'BIP0340/challenge',
aux: 'BIP0340/aux',
nonce: 'BIP0340/nonce'
};
const challenge = (...args) => modN(b2n(taggedHash(TG.cl, ...args)));
const challengeAsync = async (...args) => modN(b2n(await taggedHashAsync(TG.cl, ...args)));
/**
* Schnorr public key is just `x` coordinate of Point as per BIP340.
*/
function getPublicKeySch(privateKey) {
return schnorrGetExtPubKey(privateKey).bytes; // d'=int(sk). Fail if d'=0 or d'≥n. Ret bytes(d'⋅G)
}
// Common preparation function for both sync and async signing
function prepareSchnorrSign(message, privateKey, auxRand) {
const m = au8(message);
const { bytes: px, scalar: d } = schnorrGetExtPubKey(privateKey);
const a = au8(auxRand, 32);
return { m, px, d, a };
}
function extractK(rand) {
const k_ = modN(b2n(rand)); // Let k' = int(rand) mod n
if (k_ === 0n)
err('sign failed: k is zero'); // Fail if k' = 0.
const res = schnorrGetExtPubKey(k_); // Let R = k'⋅G.
return { rx: res.bytes, k: res.scalar };
}
// Common signature creation helper
function createSchnorrSignature(k, rx, e, d) {
const sig = new Uint8Array(64);
sig.set(rx, 0);
sig.set(n2b(modN(k + e * d)), 32);
return sig;
}
/**
* Creates Schnorr signature as per BIP340. Verifies itself before returning anything.
* auxRand is optional and is not the sole source of k generation: bad CSPRNG won't be dangerous.
*/
function signSch(message, privateKey, auxRand = etc_schnorr.randomBytes(32)) {
const { m, px, d, a } = prepareSchnorrSign(message, privateKey, auxRand);
const aux = taggedHash(TG.aux, a);
const t = n2b(d ^ b2n(aux)); // Let t be the byte-wise xor of bytes(d) and hash/aux(a)
const rand = taggedHash(TG.nonce, t, px, m); // Let rand = hash/nonce(t || bytes(P) || m)
const { rx, k } = extractK(rand);
const e = challenge(rx, px, m); // Let e = int(hash/challenge(bytes(R) || bytes(P) || m)) mod n.
const sig = createSchnorrSignature(k, rx, e, d);
if (!verifySch(sig, m, px))
err('invalid signature produced');
return sig;
}
async function signAsyncSch(message, privateKey, auxRand = etc_schnorr.randomBytes(32)) {
const { m, px, d, a } = prepareSchnorrSign(message, privateKey, auxRand);
const aux = await taggedHashAsync(TG.aux, a);
const t = n2b(d ^ b2n(aux)); // Let t be the byte-wise xor of bytes(d) and hash/aux(a)
const rand = await taggedHashAsync(TG.nonce, t, px, m); // Let rand = hash/nonce(t || bytes(P) || m)
const { rx, k } = extractK(rand);
const e = await challengeAsync(rx, px, m); // Let e = int(hash/challenge(bytes(R) || bytes(P) || m)) mod n.
const sig = createSchnorrSignature(k, rx, e, d);
// If Verify(bytes(P), m, sig) (see below) returns failure, abort
if (!(await verifyAsyncSch(sig, m, px)))
err('invalid signature produced');
return sig;
}
function prepVerif(signature, message, publicKey) {
const sig = au8(signature, 64);
const m = au8(message);
const pub = au8(publicKey, 32);
const P = lift_x(b2n(pub)); // P = lift_x(int(pk)); fail if that fails
const r = b2n(sig.subarray(0, 32)); // Let r = int(sig[0:32]); fail if r ≥ p.
if (!inRange(r, CURVE.p))
return false;
const s = b2n(sig.subarray(32, 64)); // Let s = int(sig[32:64]); fail if s ≥ n.
if (!inRange(s, N))
return false;
const input = concatB(n2b(r), pointToBytes(P), m);
return { input, P, r, s };
}
function endVerif(P, r, s, e) {
const R = G.mulAddQUns(P, s, modN(-e)); // R = s⋅G - e⋅P
if (!R || !hasEvenY(R) || R.toAffine().x !== r)
return false; // -eP == (n-e)P
return true; // Fail if is_infinite(R) / not has_even_y(R) / x(R) ≠ r.
}
/**
* Verifies Schnorr signature.
* Will swallow errors & return false except for initial type validation of arguments.
*/
function verifySch(signature, message, publicKey) {
try {
const obj = prepVerif(signature, message, publicKey) || err('failed');
const { input, P, r, s } = obj;
const e = challenge(input); // int(challenge(bytes(r)||bytes(P)||m))%n
return endVerif(P, r, s, e);
}
catch (error) {
return false;
}
}
async function verifyAsyncSch(signature, message, publicKey) {
try {
const obj = prepVerif(signature, message, publicKey) || err('failed');
const { input, P, r, s } = obj;
const e = await challengeAsync(input); // int(challenge(bytes(r)||bytes(P)||m))%n
return endVerif(P, r, s, e);
}
catch (error) {
return false;
}
}
export const schnorr = {
getPublicKey: getPublicKeySch,
sign: signSch,
verify: verifySch,
signAsync: signAsyncSch,
verifyAsync: verifyAsyncSch,
};
Loading