mirror of
https://github.com/pezkuwichain/pezkuwi-wasm.git
synced 2026-06-19 20:21:06 +00:00
feat: add @pezkuwi/scure-sr25519 with bizinikiwi signing context
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# @pezkuwi/scure-sr25519
|
||||||
|
|
||||||
|
SR25519 cryptography for PezkuwiChain with **bizinikiwi** signing context.
|
||||||
|
|
||||||
|
Fork of [@scure/sr25519](https://github.com/paulmillr/scure-sr25519) with PezkuwiChain-specific signing context.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @pezkuwi/scure-sr25519
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { getPublicKey, sign, verify, secretFromSeed } from '@pezkuwi/scure-sr25519';
|
||||||
|
|
||||||
|
// Generate keypair from 32-byte seed
|
||||||
|
const secret = secretFromSeed(seed);
|
||||||
|
const publicKey = getPublicKey(secret);
|
||||||
|
|
||||||
|
// Sign message
|
||||||
|
const signature = sign(secret, message);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
const valid = verify(message, signature, publicKey);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Difference from @scure/sr25519
|
||||||
|
|
||||||
|
This package uses `bizinikiwi` as the signing context instead of `substrate`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import { RistrettoPoint } from '@noble/curves/ed25519.js';
|
||||||
|
type Point = typeof RistrettoPoint.BASE;
|
||||||
|
type Data = string | Uint8Array;
|
||||||
|
export type RNG = (bytes: number) => Uint8Array;
|
||||||
|
declare class Strobe128 {
|
||||||
|
state: Uint8Array;
|
||||||
|
state32: Uint32Array;
|
||||||
|
pos: number;
|
||||||
|
posBegin: number;
|
||||||
|
curFlags: number;
|
||||||
|
constructor(protocolLabel: Data);
|
||||||
|
private keccakF1600;
|
||||||
|
private runF;
|
||||||
|
private absorb;
|
||||||
|
private squeeze;
|
||||||
|
private overwrite;
|
||||||
|
private beginOp;
|
||||||
|
metaAD(data: Data, more: boolean): void;
|
||||||
|
AD(data: Data, more: boolean): void;
|
||||||
|
PRF(len: number, more: boolean): Uint8Array;
|
||||||
|
KEY(data: Data, more: boolean): void;
|
||||||
|
clone(): Strobe128;
|
||||||
|
clean(): void;
|
||||||
|
}
|
||||||
|
declare class Merlin {
|
||||||
|
strobe: Strobe128;
|
||||||
|
constructor(label: Data);
|
||||||
|
appendMessage(label: Data, message: Data): void;
|
||||||
|
challengeBytes(label: Data, len: number): Uint8Array;
|
||||||
|
clean(): void;
|
||||||
|
}
|
||||||
|
declare class SigningContext extends Merlin {
|
||||||
|
private rng;
|
||||||
|
constructor(name: string, rng?: RNG);
|
||||||
|
label(label: Data): void;
|
||||||
|
bytes(bytes: Uint8Array): this;
|
||||||
|
protoName(label: Data): void;
|
||||||
|
commitPoint(label: Data, point: Point): void;
|
||||||
|
challengeScalar(label: Data): bigint;
|
||||||
|
witnessScalar(label: Data, nonceSeeds?: Uint8Array[]): bigint;
|
||||||
|
witnessBytes(label: Data, len: number, nonceSeeds?: Uint8Array[]): Uint8Array;
|
||||||
|
}
|
||||||
|
export declare function getPublicKey(secretKey: Uint8Array): Uint8Array;
|
||||||
|
export declare function secretFromSeed(seed: Uint8Array): Uint8Array;
|
||||||
|
export declare function fromKeypair(pair: Uint8Array): Uint8Array;
|
||||||
|
export declare function sign(secretKey: Uint8Array, message: Uint8Array, rng?: RNG): Uint8Array;
|
||||||
|
export declare function verify(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
|
||||||
|
export declare function getSharedSecret(secretKey: Uint8Array, publicKey: Uint8Array): Uint8Array;
|
||||||
|
export declare const HDKD: {
|
||||||
|
secretSoft(secretKey: Uint8Array, chainCode: Uint8Array, rng?: RNG): Uint8Array;
|
||||||
|
publicSoft(publicKey: Uint8Array, chainCode: Uint8Array): Uint8Array;
|
||||||
|
secretHard(secretKey: Uint8Array, chainCode: Uint8Array): Uint8Array;
|
||||||
|
};
|
||||||
|
export declare const vrf: {
|
||||||
|
sign(msg: Uint8Array, secretKey: Uint8Array, ctx: Uint8Array, extra: Uint8Array, rng: RNG): Uint8Array;
|
||||||
|
verify(msg: Uint8Array, signature: Uint8Array, publicKey: Uint8Array, ctx?: Uint8Array, extra?: Uint8Array, rng?: RNG): boolean;
|
||||||
|
};
|
||||||
|
export declare const __tests: {
|
||||||
|
Strobe128: typeof Strobe128;
|
||||||
|
Merlin: typeof Merlin;
|
||||||
|
SigningContext: typeof SigningContext;
|
||||||
|
};
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../index.ts"],"names":[],"mappings":"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"}
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
/**
|
||||||
|
* Minimal JS implementation of sr25519 cryptography for Polkadot.
|
||||||
|
*
|
||||||
|
* Uses [Merlin](https://merlin.cool/index.html),
|
||||||
|
* a transcript construction, built on [Strobe](https://strobe.sourceforge.io).
|
||||||
|
* Merlin ensures two parties agree on the same state when communicating.
|
||||||
|
*
|
||||||
|
* More: https://wiki.polkadot.network/docs/learn-cryptography.
|
||||||
|
*/
|
||||||
|
import { mod } from '@noble/curves/abstract/modular.js';
|
||||||
|
import { aInRange, bitMask, bytesToNumberLE, equalBytes, isBytes, numberToBytesLE, } from '@noble/curves/abstract/utils.js';
|
||||||
|
import { ed25519, RistrettoPoint } from '@noble/curves/ed25519.js';
|
||||||
|
import { keccakP } from '@noble/hashes/sha3.js';
|
||||||
|
import { sha512 } from '@noble/hashes/sha512.js';
|
||||||
|
import { concatBytes, randomBytes, u32, utf8ToBytes } from '@noble/hashes/utils.js';
|
||||||
|
// prettier-ignore
|
||||||
|
const _0n = BigInt(0), _3n = BigInt(3);
|
||||||
|
function toData(d) {
|
||||||
|
if (typeof d === 'string')
|
||||||
|
return utf8ToBytes(d);
|
||||||
|
if (isBytes(d))
|
||||||
|
return d;
|
||||||
|
throw new Error('Wrong data');
|
||||||
|
}
|
||||||
|
// Could've used bytes from hashes/assert, but we add extra arg
|
||||||
|
function abytes(title, b, ...lengths) {
|
||||||
|
if (!isBytes(b))
|
||||||
|
throw new Error(`${title}: Uint8Array expected`);
|
||||||
|
if (lengths.length && !lengths.includes(b.length))
|
||||||
|
throw new Error(`${title}: Uint8Array expected of length ${lengths}, not of length=${b.length}`);
|
||||||
|
}
|
||||||
|
function checkU32(title, n) {
|
||||||
|
if (!Number.isSafeInteger(n) || n < 0 || n > 4294967295)
|
||||||
|
throw new Error(`${title}: wrong u32 integer: ${n}`);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
function cleanBytes(...list) {
|
||||||
|
for (const t of list)
|
||||||
|
t.fill(0);
|
||||||
|
}
|
||||||
|
const EMPTY = Uint8Array.of();
|
||||||
|
const CURVE_ORDER = ed25519.CURVE.n;
|
||||||
|
function parseScalar(title, bytes) {
|
||||||
|
abytes(title, bytes, 32);
|
||||||
|
const n = bytesToNumberLE(bytes);
|
||||||
|
aInRange(title, n, _0n, CURVE_ORDER);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
const modN = (n) => mod(n, CURVE_ORDER);
|
||||||
|
// STROBE128 (minimal version required for Merlin)
|
||||||
|
// - https://strobe.sourceforge.io/specs/
|
||||||
|
// We can implement full version, but seems nobody uses this much.
|
||||||
|
const STROBE_R = 166;
|
||||||
|
const Flags = {
|
||||||
|
I: 1,
|
||||||
|
A: 1 << 1,
|
||||||
|
C: 1 << 2,
|
||||||
|
T: 1 << 3,
|
||||||
|
M: 1 << 4,
|
||||||
|
K: 1 << 5,
|
||||||
|
};
|
||||||
|
// Differences: suffix, additional methods/flags
|
||||||
|
class Strobe128 {
|
||||||
|
constructor(protocolLabel) {
|
||||||
|
this.state = new Uint8Array(200);
|
||||||
|
this.pos = 0;
|
||||||
|
this.posBegin = 0;
|
||||||
|
this.curFlags = 0;
|
||||||
|
this.state.set([1, STROBE_R + 2, 1, 0, 1, 96], 0);
|
||||||
|
this.state.set(utf8ToBytes('STROBEv1.0.2'), 6);
|
||||||
|
this.state32 = u32(this.state);
|
||||||
|
this.keccakF1600();
|
||||||
|
this.metaAD(protocolLabel, false);
|
||||||
|
}
|
||||||
|
keccakF1600() {
|
||||||
|
keccakP(this.state32);
|
||||||
|
}
|
||||||
|
runF() {
|
||||||
|
this.state[this.pos] ^= this.posBegin;
|
||||||
|
this.state[this.pos + 1] ^= 0x04;
|
||||||
|
this.state[STROBE_R + 1] ^= 0x80;
|
||||||
|
this.keccakF1600();
|
||||||
|
this.pos = 0;
|
||||||
|
this.posBegin = 0;
|
||||||
|
}
|
||||||
|
// keccak.update()
|
||||||
|
absorb(data) {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
this.state[this.pos++] ^= data[i];
|
||||||
|
if (this.pos === STROBE_R)
|
||||||
|
this.runF();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// keccak.xof()
|
||||||
|
squeeze(len) {
|
||||||
|
const data = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
data[i] = this.state[this.pos];
|
||||||
|
this.state[this.pos++] = 0;
|
||||||
|
if (this.pos === STROBE_R)
|
||||||
|
this.runF();
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
overwrite(data) {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
this.state[this.pos++] = data[i];
|
||||||
|
if (this.pos === STROBE_R)
|
||||||
|
this.runF();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beginOp(flags, more) {
|
||||||
|
if (more) {
|
||||||
|
if (this.curFlags !== flags) {
|
||||||
|
throw new Error(`Continued op with changed flags from ${this.curFlags.toString(2)} to ${flags.toString(2)}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((flags & Flags.T) !== 0)
|
||||||
|
throw new Error('T flag is not supported');
|
||||||
|
const oldBegin = this.posBegin;
|
||||||
|
this.posBegin = this.pos + 1;
|
||||||
|
this.curFlags = flags;
|
||||||
|
this.absorb(new Uint8Array([oldBegin, flags]));
|
||||||
|
const forceF = (flags & (Flags.C | Flags.K)) !== 0;
|
||||||
|
if (forceF && this.pos !== 0)
|
||||||
|
this.runF();
|
||||||
|
}
|
||||||
|
// Public API
|
||||||
|
metaAD(data, more) {
|
||||||
|
this.beginOp(Flags.M | Flags.A, more);
|
||||||
|
this.absorb(toData(data));
|
||||||
|
}
|
||||||
|
AD(data, more) {
|
||||||
|
this.beginOp(Flags.A, more);
|
||||||
|
this.absorb(toData(data));
|
||||||
|
}
|
||||||
|
PRF(len, more) {
|
||||||
|
this.beginOp(Flags.I | Flags.A | Flags.C, more);
|
||||||
|
return this.squeeze(len);
|
||||||
|
}
|
||||||
|
KEY(data, more) {
|
||||||
|
this.beginOp(Flags.A | Flags.C, more);
|
||||||
|
this.overwrite(toData(data));
|
||||||
|
}
|
||||||
|
// Utils
|
||||||
|
clone() {
|
||||||
|
const n = new Strobe128('0'); // tmp
|
||||||
|
n.pos = this.pos;
|
||||||
|
n.posBegin = this.posBegin;
|
||||||
|
n.state.set(this.state);
|
||||||
|
n.curFlags = this.curFlags;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
clean() {
|
||||||
|
this.state.fill(0); // also clears state32, because same buffer
|
||||||
|
this.pos = 0;
|
||||||
|
this.curFlags = 0;
|
||||||
|
this.posBegin = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// /STROBE128
|
||||||
|
// Merlin
|
||||||
|
// https://merlin.cool/index.html
|
||||||
|
class Merlin {
|
||||||
|
constructor(label) {
|
||||||
|
this.strobe = new Strobe128('Merlin v1.0');
|
||||||
|
this.appendMessage('dom-sep', label);
|
||||||
|
}
|
||||||
|
appendMessage(label, message) {
|
||||||
|
this.strobe.metaAD(label, false);
|
||||||
|
checkU32('Merlin.appendMessage', message.length);
|
||||||
|
this.strobe.metaAD(numberToBytesLE(message.length, 4), true);
|
||||||
|
this.strobe.AD(message, false);
|
||||||
|
}
|
||||||
|
challengeBytes(label, len) {
|
||||||
|
this.strobe.metaAD(label, false);
|
||||||
|
checkU32('Merlin.challengeBytes', len);
|
||||||
|
this.strobe.metaAD(numberToBytesLE(len, 4), true);
|
||||||
|
return this.strobe.PRF(len, false);
|
||||||
|
}
|
||||||
|
clean() {
|
||||||
|
this.strobe.clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// /Merlin
|
||||||
|
// Merlin signging context/transcript (sr25519 specific stuff, Merlin and Strobe are generic (but minimal))
|
||||||
|
class SigningContext extends Merlin {
|
||||||
|
constructor(name, rng = randomBytes) {
|
||||||
|
super(name);
|
||||||
|
this.rng = rng;
|
||||||
|
}
|
||||||
|
label(label) {
|
||||||
|
this.appendMessage('', label);
|
||||||
|
}
|
||||||
|
bytes(bytes) {
|
||||||
|
this.appendMessage('sign-bytes', bytes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
protoName(label) {
|
||||||
|
this.appendMessage('proto-name', label);
|
||||||
|
}
|
||||||
|
commitPoint(label, point) {
|
||||||
|
this.appendMessage(label, point.toRawBytes());
|
||||||
|
}
|
||||||
|
challengeScalar(label) {
|
||||||
|
return modN(bytesToNumberLE(this.challengeBytes(label, 64)));
|
||||||
|
}
|
||||||
|
witnessScalar(label, nonceSeeds = []) {
|
||||||
|
return modN(bytesToNumberLE(this.witnessBytes(label, 64, nonceSeeds)));
|
||||||
|
}
|
||||||
|
witnessBytes(label, len, nonceSeeds = []) {
|
||||||
|
checkU32('SigningContext.witnessBytes', len);
|
||||||
|
const strobeRng = this.strobe.clone();
|
||||||
|
for (const ns of nonceSeeds) {
|
||||||
|
strobeRng.metaAD(label, false);
|
||||||
|
checkU32('SigningContext.witnessBytes nonce length', ns.length);
|
||||||
|
strobeRng.metaAD(numberToBytesLE(ns.length, 4), true);
|
||||||
|
strobeRng.KEY(ns, false);
|
||||||
|
}
|
||||||
|
const random = this.rng(32);
|
||||||
|
strobeRng.metaAD('rng', false);
|
||||||
|
strobeRng.KEY(random, false);
|
||||||
|
strobeRng.metaAD(numberToBytesLE(len, 4), false);
|
||||||
|
return strobeRng.PRF(len, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// /Merlin signing context
|
||||||
|
const MASK = bitMask(256);
|
||||||
|
// == (n * CURVE.h) % CURVE_BIT_MASK
|
||||||
|
const encodeScalar = (n) => numberToBytesLE((n << _3n) & MASK, 32);
|
||||||
|
// n / CURVE.h
|
||||||
|
const decodeScalar = (n) => bytesToNumberLE(n) >> _3n;
|
||||||
|
// NOTE: secretKey is 64 bytes (key + nonce). This required for HDKD, since key can be derived not only from seed, but from other keys.
|
||||||
|
export function getPublicKey(secretKey) {
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
const scalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
return RistrettoPoint.BASE.multiply(scalar).toRawBytes();
|
||||||
|
}
|
||||||
|
export function secretFromSeed(seed) {
|
||||||
|
abytes('seed', seed, 32);
|
||||||
|
const r = sha512(seed);
|
||||||
|
// NOTE: different from ed25519
|
||||||
|
r[0] &= 248;
|
||||||
|
r[31] &= 63;
|
||||||
|
r[31] |= 64;
|
||||||
|
// this will strip upper 3 bits and lower 3 bits
|
||||||
|
const key = encodeScalar(decodeScalar(r.subarray(0, 32)));
|
||||||
|
const nonce = r.subarray(32, 64);
|
||||||
|
const res = concatBytes(key, nonce);
|
||||||
|
cleanBytes(key, nonce, r);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
// Seems like ed25519 keypair? Generates keypair from other keypair in ed25519 format
|
||||||
|
// NOTE: not exported from wasm. Do we need this at all?
|
||||||
|
export function fromKeypair(pair) {
|
||||||
|
abytes('keypair', pair, 96);
|
||||||
|
const sk = pair.subarray(0, 32);
|
||||||
|
const nonce = pair.subarray(32, 64);
|
||||||
|
const pubBytes = pair.subarray(64, 96);
|
||||||
|
const key = encodeScalar(bytesToNumberLE(sk));
|
||||||
|
const realPub = getPublicKey(pair.subarray(0, 64));
|
||||||
|
if (!equalBytes(pubBytes, realPub))
|
||||||
|
throw new Error('wrong public key');
|
||||||
|
// No need to clean since subarray's
|
||||||
|
return concatBytes(key, nonce, realPub);
|
||||||
|
}
|
||||||
|
// Basic sign. NOTE: context is currently constant. Please open issue if you need different one.
|
||||||
|
const BIZINIKIWI_CONTEXT = utf8ToBytes('bizinikiwi');
|
||||||
|
export function sign(secretKey, message, rng = randomBytes) {
|
||||||
|
abytes('message', message);
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
const t = new SigningContext('SigningContext', rng);
|
||||||
|
t.label(BIZINIKIWI_CONTEXT);
|
||||||
|
t.bytes(message);
|
||||||
|
const keyScalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
const nonce = secretKey.subarray(32, 64);
|
||||||
|
const pubPoint = RistrettoPoint.fromHex(getPublicKey(secretKey));
|
||||||
|
t.protoName('Schnorr-sig');
|
||||||
|
t.commitPoint('sign:pk', pubPoint);
|
||||||
|
const r = t.witnessScalar('signing', [nonce]);
|
||||||
|
const R = RistrettoPoint.BASE.multiply(r);
|
||||||
|
t.commitPoint('sign:R', R);
|
||||||
|
const k = t.challengeScalar('sign:c');
|
||||||
|
const s = modN(k * keyScalar + r);
|
||||||
|
const res = concatBytes(R.toRawBytes(), numberToBytesLE(s, 32));
|
||||||
|
res[63] |= 128; // add Schnorrkel marker
|
||||||
|
t.clean();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
export function verify(message, signature, publicKey) {
|
||||||
|
abytes('message', message);
|
||||||
|
abytes('signature', signature, 64);
|
||||||
|
abytes('publicKey', publicKey, 32);
|
||||||
|
if ((signature[63] & 128) === 0)
|
||||||
|
throw new Error('Schnorrkel marker missing');
|
||||||
|
const sBytes = Uint8Array.from(signature.subarray(32, 64)); // copy before modification
|
||||||
|
sBytes[31] &= 127; // remove Schnorrkel marker
|
||||||
|
const R = RistrettoPoint.fromHex(signature.subarray(0, 32));
|
||||||
|
const s = bytesToNumberLE(sBytes);
|
||||||
|
aInRange('s', s, _0n, CURVE_ORDER); // Just in case, it will be checked at multiplication later
|
||||||
|
const t = new SigningContext('SigningContext');
|
||||||
|
t.label(BIZINIKIWI_CONTEXT);
|
||||||
|
t.bytes(message);
|
||||||
|
const pubPoint = RistrettoPoint.fromHex(publicKey);
|
||||||
|
if (pubPoint.equals(RistrettoPoint.ZERO))
|
||||||
|
return false;
|
||||||
|
t.protoName('Schnorr-sig');
|
||||||
|
t.commitPoint('sign:pk', pubPoint);
|
||||||
|
t.commitPoint('sign:R', R);
|
||||||
|
const k = t.challengeScalar('sign:c');
|
||||||
|
const sP = RistrettoPoint.BASE.multiply(s);
|
||||||
|
const RR = pubPoint.negate().multiply(k).add(sP);
|
||||||
|
t.clean();
|
||||||
|
cleanBytes(sBytes);
|
||||||
|
return RR.equals(R);
|
||||||
|
}
|
||||||
|
export function getSharedSecret(secretKey, publicKey) {
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
abytes('publicKey', publicKey, 32);
|
||||||
|
const keyScalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
const pubPoint = RistrettoPoint.fromHex(publicKey);
|
||||||
|
if (pubPoint.equals(RistrettoPoint.ZERO))
|
||||||
|
throw new Error('wrong public key (infinity)');
|
||||||
|
return pubPoint.multiply(keyScalar).toRawBytes();
|
||||||
|
}
|
||||||
|
// Derive
|
||||||
|
export const HDKD = {
|
||||||
|
secretSoft(secretKey, chainCode, rng = randomBytes) {
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
abytes('chainCode', chainCode, 32);
|
||||||
|
const masterScalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
const masterNonce = secretKey.subarray(32, 64);
|
||||||
|
const pubPoint = RistrettoPoint.fromHex(getPublicKey(secretKey));
|
||||||
|
const t = new SigningContext('SchnorrRistrettoHDKD', rng);
|
||||||
|
t.bytes(EMPTY);
|
||||||
|
t.appendMessage('chain-code', chainCode);
|
||||||
|
t.commitPoint('public-key', pubPoint);
|
||||||
|
const scalar = t.challengeScalar('HDKD-scalar');
|
||||||
|
const hdkdChainCode = t.challengeBytes('HDKD-chaincode', 32);
|
||||||
|
const nonceSeed = concatBytes(numberToBytesLE(masterScalar, 32), masterNonce);
|
||||||
|
const nonce = t.witnessBytes('HDKD-nonce', 32, [masterNonce, nonceSeed]);
|
||||||
|
const key = encodeScalar(modN(masterScalar + scalar));
|
||||||
|
const res = concatBytes(key, nonce);
|
||||||
|
cleanBytes(key, nonce, nonceSeed, hdkdChainCode);
|
||||||
|
t.clean();
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
publicSoft(publicKey, chainCode) {
|
||||||
|
abytes('publicKey', publicKey, 32);
|
||||||
|
abytes('chainCode', chainCode, 32);
|
||||||
|
const pubPoint = RistrettoPoint.fromHex(publicKey);
|
||||||
|
const t = new SigningContext('SchnorrRistrettoHDKD');
|
||||||
|
t.bytes(EMPTY);
|
||||||
|
t.appendMessage('chain-code', chainCode);
|
||||||
|
t.commitPoint('public-key', pubPoint);
|
||||||
|
const scalar = t.challengeScalar('HDKD-scalar');
|
||||||
|
t.challengeBytes('HDKD-chaincode', 32);
|
||||||
|
t.clean();
|
||||||
|
return pubPoint.add(RistrettoPoint.BASE.multiply(scalar)).toRawBytes();
|
||||||
|
},
|
||||||
|
secretHard(secretKey, chainCode) {
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
abytes('chainCode', chainCode, 32);
|
||||||
|
const key = numberToBytesLE(decodeScalar(secretKey.subarray(0, 32)), 32);
|
||||||
|
const t = new SigningContext('SchnorrRistrettoHDKD');
|
||||||
|
t.bytes(EMPTY);
|
||||||
|
t.appendMessage('chain-code', chainCode);
|
||||||
|
t.appendMessage('secret-key', key);
|
||||||
|
const msk = t.challengeBytes('HDKD-hard', 32);
|
||||||
|
const hdkdChainCode = t.challengeBytes('HDKD-chaincode', 32);
|
||||||
|
t.clean();
|
||||||
|
const res = secretFromSeed(msk);
|
||||||
|
cleanBytes(key, msk, hdkdChainCode);
|
||||||
|
t.clean();
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dleq = {
|
||||||
|
proove(keyScalar, nonce, pubPoint, t, input, output) {
|
||||||
|
t.protoName('DLEQProof');
|
||||||
|
t.commitPoint('vrf:h', input);
|
||||||
|
const r = t.witnessScalar(`proving${'\0'}0`, [nonce]);
|
||||||
|
const R = RistrettoPoint.BASE.multiply(r);
|
||||||
|
t.commitPoint('vrf:R=g^r', R);
|
||||||
|
const Hr = input.multiply(r);
|
||||||
|
t.commitPoint('vrf:h^r', Hr);
|
||||||
|
t.commitPoint('vrf:pk', pubPoint);
|
||||||
|
t.commitPoint('vrf:h^sk', output);
|
||||||
|
const c = t.challengeScalar('prove');
|
||||||
|
const s = modN(r - c * keyScalar);
|
||||||
|
return { proof: { c, s }, proofBatchable: { R, Hr, s } };
|
||||||
|
},
|
||||||
|
verify(pubPoint, t, input, output, proof) {
|
||||||
|
if (pubPoint.equals(RistrettoPoint.ZERO))
|
||||||
|
return false;
|
||||||
|
t.protoName('DLEQProof');
|
||||||
|
t.commitPoint('vrf:h', input);
|
||||||
|
const R = pubPoint.multiply(proof.c).add(RistrettoPoint.BASE.multiply(proof.s));
|
||||||
|
t.commitPoint('vrf:R=g^r', R);
|
||||||
|
const Hr = output.multiply(proof.c).add(input.multiply(proof.s));
|
||||||
|
t.commitPoint('vrf:h^r', Hr);
|
||||||
|
t.commitPoint('vrf:pk', pubPoint);
|
||||||
|
t.commitPoint('vrf:h^sk', output);
|
||||||
|
const realC = t.challengeScalar('prove');
|
||||||
|
if (proof.c === realC)
|
||||||
|
return { R, Hr, s: proof.s }; // proofBatchable
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// VRF: Verifiable Random Function
|
||||||
|
function initVRF(ctx, msg, extra, pubPoint, rng = randomBytes) {
|
||||||
|
const t = new SigningContext('SigningContext', rng);
|
||||||
|
t.label(ctx);
|
||||||
|
t.bytes(msg);
|
||||||
|
t.commitPoint('vrf-nm-pk', pubPoint);
|
||||||
|
const hash = t.challengeBytes('VRFHash', 64);
|
||||||
|
const input = RistrettoPoint.hashToCurve(hash);
|
||||||
|
const transcript = new SigningContext('VRF', rng);
|
||||||
|
if (extra.length)
|
||||||
|
transcript.label(extra);
|
||||||
|
t.clean();
|
||||||
|
cleanBytes(hash);
|
||||||
|
return { input, t: transcript };
|
||||||
|
}
|
||||||
|
export const vrf = {
|
||||||
|
sign(msg, secretKey, ctx = EMPTY, extra = EMPTY, rng = randomBytes) {
|
||||||
|
abytes('msg', msg);
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
abytes('ctx', ctx);
|
||||||
|
abytes('extra', extra);
|
||||||
|
const keyScalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
const nonce = secretKey.subarray(32, 64);
|
||||||
|
const pubPoint = RistrettoPoint.fromHex(getPublicKey(secretKey));
|
||||||
|
const { input, t } = initVRF(ctx, msg, extra, pubPoint, rng);
|
||||||
|
const output = input.multiply(keyScalar);
|
||||||
|
const p = { input, output };
|
||||||
|
const { proof } = dleq.proove(keyScalar, nonce, pubPoint, t, input, output);
|
||||||
|
const cBytes = numberToBytesLE(proof.c, 32);
|
||||||
|
const sBytes = numberToBytesLE(proof.s, 32);
|
||||||
|
const res = concatBytes(p.output.toRawBytes(), cBytes, sBytes);
|
||||||
|
cleanBytes(nonce, cBytes, sBytes);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
verify(msg, signature, publicKey, ctx = EMPTY, extra = EMPTY, rng = randomBytes) {
|
||||||
|
abytes('msg', msg);
|
||||||
|
abytes('signature', signature, 96); // O(point) || c(scalar) || s(scalar)
|
||||||
|
abytes('pubkey', publicKey, 32);
|
||||||
|
abytes('ctx', ctx);
|
||||||
|
abytes('extra', extra);
|
||||||
|
const pubPoint = RistrettoPoint.fromHex(publicKey);
|
||||||
|
if (pubPoint.equals(RistrettoPoint.ZERO))
|
||||||
|
return false;
|
||||||
|
const proof = {
|
||||||
|
c: parseScalar('signature.c', signature.subarray(32, 64)),
|
||||||
|
s: parseScalar('signature.s', signature.subarray(64, 96)),
|
||||||
|
};
|
||||||
|
const { input, t } = initVRF(ctx, msg, extra, pubPoint, rng);
|
||||||
|
const output = RistrettoPoint.fromHex(signature.subarray(0, 32));
|
||||||
|
if (output.equals(RistrettoPoint.ZERO))
|
||||||
|
throw new Error('vrf.verify: wrong output point (identity)');
|
||||||
|
const proofBatchable = dleq.verify(pubPoint, t, input, output, proof);
|
||||||
|
return proofBatchable === false ? false : true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// NOTE: for tests only, don't use
|
||||||
|
export const __tests = {
|
||||||
|
Strobe128,
|
||||||
|
Merlin,
|
||||||
|
SigningContext,
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"browser": {
|
||||||
|
"crypto": false
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import { RistrettoPoint } from '@noble/curves/ed25519.js';
|
||||||
|
type Point = typeof RistrettoPoint.BASE;
|
||||||
|
type Data = string | Uint8Array;
|
||||||
|
export type RNG = (bytes: number) => Uint8Array;
|
||||||
|
declare class Strobe128 {
|
||||||
|
state: Uint8Array;
|
||||||
|
state32: Uint32Array;
|
||||||
|
pos: number;
|
||||||
|
posBegin: number;
|
||||||
|
curFlags: number;
|
||||||
|
constructor(protocolLabel: Data);
|
||||||
|
private keccakF1600;
|
||||||
|
private runF;
|
||||||
|
private absorb;
|
||||||
|
private squeeze;
|
||||||
|
private overwrite;
|
||||||
|
private beginOp;
|
||||||
|
metaAD(data: Data, more: boolean): void;
|
||||||
|
AD(data: Data, more: boolean): void;
|
||||||
|
PRF(len: number, more: boolean): Uint8Array;
|
||||||
|
KEY(data: Data, more: boolean): void;
|
||||||
|
clone(): Strobe128;
|
||||||
|
clean(): void;
|
||||||
|
}
|
||||||
|
declare class Merlin {
|
||||||
|
strobe: Strobe128;
|
||||||
|
constructor(label: Data);
|
||||||
|
appendMessage(label: Data, message: Data): void;
|
||||||
|
challengeBytes(label: Data, len: number): Uint8Array;
|
||||||
|
clean(): void;
|
||||||
|
}
|
||||||
|
declare class SigningContext extends Merlin {
|
||||||
|
private rng;
|
||||||
|
constructor(name: string, rng?: RNG);
|
||||||
|
label(label: Data): void;
|
||||||
|
bytes(bytes: Uint8Array): this;
|
||||||
|
protoName(label: Data): void;
|
||||||
|
commitPoint(label: Data, point: Point): void;
|
||||||
|
challengeScalar(label: Data): bigint;
|
||||||
|
witnessScalar(label: Data, nonceSeeds?: Uint8Array[]): bigint;
|
||||||
|
witnessBytes(label: Data, len: number, nonceSeeds?: Uint8Array[]): Uint8Array;
|
||||||
|
}
|
||||||
|
export declare function getPublicKey(secretKey: Uint8Array): Uint8Array;
|
||||||
|
export declare function secretFromSeed(seed: Uint8Array): Uint8Array;
|
||||||
|
export declare function fromKeypair(pair: Uint8Array): Uint8Array;
|
||||||
|
export declare function sign(secretKey: Uint8Array, message: Uint8Array, rng?: RNG): Uint8Array;
|
||||||
|
export declare function verify(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
|
||||||
|
export declare function getSharedSecret(secretKey: Uint8Array, publicKey: Uint8Array): Uint8Array;
|
||||||
|
export declare const HDKD: {
|
||||||
|
secretSoft(secretKey: Uint8Array, chainCode: Uint8Array, rng?: RNG): Uint8Array;
|
||||||
|
publicSoft(publicKey: Uint8Array, chainCode: Uint8Array): Uint8Array;
|
||||||
|
secretHard(secretKey: Uint8Array, chainCode: Uint8Array): Uint8Array;
|
||||||
|
};
|
||||||
|
export declare const vrf: {
|
||||||
|
sign(msg: Uint8Array, secretKey: Uint8Array, ctx: Uint8Array, extra: Uint8Array, rng: RNG): Uint8Array;
|
||||||
|
verify(msg: Uint8Array, signature: Uint8Array, publicKey: Uint8Array, ctx?: Uint8Array, extra?: Uint8Array, rng?: RNG): boolean;
|
||||||
|
};
|
||||||
|
export declare const __tests: {
|
||||||
|
Strobe128: typeof Strobe128;
|
||||||
|
Merlin: typeof Merlin;
|
||||||
|
SigningContext: typeof SigningContext;
|
||||||
|
};
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.__tests = exports.vrf = exports.HDKD = void 0;
|
||||||
|
exports.getPublicKey = getPublicKey;
|
||||||
|
exports.secretFromSeed = secretFromSeed;
|
||||||
|
exports.fromKeypair = fromKeypair;
|
||||||
|
exports.sign = sign;
|
||||||
|
exports.verify = verify;
|
||||||
|
exports.getSharedSecret = getSharedSecret;
|
||||||
|
/**
|
||||||
|
* Minimal JS implementation of sr25519 cryptography for Polkadot.
|
||||||
|
*
|
||||||
|
* Uses [Merlin](https://merlin.cool/index.html),
|
||||||
|
* a transcript construction, built on [Strobe](https://strobe.sourceforge.io).
|
||||||
|
* Merlin ensures two parties agree on the same state when communicating.
|
||||||
|
*
|
||||||
|
* More: https://wiki.polkadot.network/docs/learn-cryptography.
|
||||||
|
*/
|
||||||
|
const modular_js_1 = require("@noble/curves/abstract/modular.js");
|
||||||
|
const utils_js_1 = require("@noble/curves/abstract/utils.js");
|
||||||
|
const ed25519_js_1 = require("@noble/curves/ed25519.js");
|
||||||
|
const sha3_js_1 = require("@noble/hashes/sha3.js");
|
||||||
|
const sha512_js_1 = require("@noble/hashes/sha512.js");
|
||||||
|
const utils_js_2 = require("@noble/hashes/utils.js");
|
||||||
|
// prettier-ignore
|
||||||
|
const _0n = BigInt(0), _3n = BigInt(3);
|
||||||
|
function toData(d) {
|
||||||
|
if (typeof d === 'string')
|
||||||
|
return (0, utils_js_2.utf8ToBytes)(d);
|
||||||
|
if ((0, utils_js_1.isBytes)(d))
|
||||||
|
return d;
|
||||||
|
throw new Error('Wrong data');
|
||||||
|
}
|
||||||
|
// Could've used bytes from hashes/assert, but we add extra arg
|
||||||
|
function abytes(title, b, ...lengths) {
|
||||||
|
if (!(0, utils_js_1.isBytes)(b))
|
||||||
|
throw new Error(`${title}: Uint8Array expected`);
|
||||||
|
if (lengths.length && !lengths.includes(b.length))
|
||||||
|
throw new Error(`${title}: Uint8Array expected of length ${lengths}, not of length=${b.length}`);
|
||||||
|
}
|
||||||
|
function checkU32(title, n) {
|
||||||
|
if (!Number.isSafeInteger(n) || n < 0 || n > 4294967295)
|
||||||
|
throw new Error(`${title}: wrong u32 integer: ${n}`);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
function cleanBytes(...list) {
|
||||||
|
for (const t of list)
|
||||||
|
t.fill(0);
|
||||||
|
}
|
||||||
|
const EMPTY = Uint8Array.of();
|
||||||
|
const CURVE_ORDER = ed25519_js_1.ed25519.CURVE.n;
|
||||||
|
function parseScalar(title, bytes) {
|
||||||
|
abytes(title, bytes, 32);
|
||||||
|
const n = (0, utils_js_1.bytesToNumberLE)(bytes);
|
||||||
|
(0, utils_js_1.aInRange)(title, n, _0n, CURVE_ORDER);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
const modN = (n) => (0, modular_js_1.mod)(n, CURVE_ORDER);
|
||||||
|
// STROBE128 (minimal version required for Merlin)
|
||||||
|
// - https://strobe.sourceforge.io/specs/
|
||||||
|
// We can implement full version, but seems nobody uses this much.
|
||||||
|
const STROBE_R = 166;
|
||||||
|
const Flags = {
|
||||||
|
I: 1,
|
||||||
|
A: 1 << 1,
|
||||||
|
C: 1 << 2,
|
||||||
|
T: 1 << 3,
|
||||||
|
M: 1 << 4,
|
||||||
|
K: 1 << 5,
|
||||||
|
};
|
||||||
|
// Differences: suffix, additional methods/flags
|
||||||
|
class Strobe128 {
|
||||||
|
constructor(protocolLabel) {
|
||||||
|
this.state = new Uint8Array(200);
|
||||||
|
this.pos = 0;
|
||||||
|
this.posBegin = 0;
|
||||||
|
this.curFlags = 0;
|
||||||
|
this.state.set([1, STROBE_R + 2, 1, 0, 1, 96], 0);
|
||||||
|
this.state.set((0, utils_js_2.utf8ToBytes)('STROBEv1.0.2'), 6);
|
||||||
|
this.state32 = (0, utils_js_2.u32)(this.state);
|
||||||
|
this.keccakF1600();
|
||||||
|
this.metaAD(protocolLabel, false);
|
||||||
|
}
|
||||||
|
keccakF1600() {
|
||||||
|
(0, sha3_js_1.keccakP)(this.state32);
|
||||||
|
}
|
||||||
|
runF() {
|
||||||
|
this.state[this.pos] ^= this.posBegin;
|
||||||
|
this.state[this.pos + 1] ^= 0x04;
|
||||||
|
this.state[STROBE_R + 1] ^= 0x80;
|
||||||
|
this.keccakF1600();
|
||||||
|
this.pos = 0;
|
||||||
|
this.posBegin = 0;
|
||||||
|
}
|
||||||
|
// keccak.update()
|
||||||
|
absorb(data) {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
this.state[this.pos++] ^= data[i];
|
||||||
|
if (this.pos === STROBE_R)
|
||||||
|
this.runF();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// keccak.xof()
|
||||||
|
squeeze(len) {
|
||||||
|
const data = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
data[i] = this.state[this.pos];
|
||||||
|
this.state[this.pos++] = 0;
|
||||||
|
if (this.pos === STROBE_R)
|
||||||
|
this.runF();
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
overwrite(data) {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
this.state[this.pos++] = data[i];
|
||||||
|
if (this.pos === STROBE_R)
|
||||||
|
this.runF();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beginOp(flags, more) {
|
||||||
|
if (more) {
|
||||||
|
if (this.curFlags !== flags) {
|
||||||
|
throw new Error(`Continued op with changed flags from ${this.curFlags.toString(2)} to ${flags.toString(2)}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((flags & Flags.T) !== 0)
|
||||||
|
throw new Error('T flag is not supported');
|
||||||
|
const oldBegin = this.posBegin;
|
||||||
|
this.posBegin = this.pos + 1;
|
||||||
|
this.curFlags = flags;
|
||||||
|
this.absorb(new Uint8Array([oldBegin, flags]));
|
||||||
|
const forceF = (flags & (Flags.C | Flags.K)) !== 0;
|
||||||
|
if (forceF && this.pos !== 0)
|
||||||
|
this.runF();
|
||||||
|
}
|
||||||
|
// Public API
|
||||||
|
metaAD(data, more) {
|
||||||
|
this.beginOp(Flags.M | Flags.A, more);
|
||||||
|
this.absorb(toData(data));
|
||||||
|
}
|
||||||
|
AD(data, more) {
|
||||||
|
this.beginOp(Flags.A, more);
|
||||||
|
this.absorb(toData(data));
|
||||||
|
}
|
||||||
|
PRF(len, more) {
|
||||||
|
this.beginOp(Flags.I | Flags.A | Flags.C, more);
|
||||||
|
return this.squeeze(len);
|
||||||
|
}
|
||||||
|
KEY(data, more) {
|
||||||
|
this.beginOp(Flags.A | Flags.C, more);
|
||||||
|
this.overwrite(toData(data));
|
||||||
|
}
|
||||||
|
// Utils
|
||||||
|
clone() {
|
||||||
|
const n = new Strobe128('0'); // tmp
|
||||||
|
n.pos = this.pos;
|
||||||
|
n.posBegin = this.posBegin;
|
||||||
|
n.state.set(this.state);
|
||||||
|
n.curFlags = this.curFlags;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
clean() {
|
||||||
|
this.state.fill(0); // also clears state32, because same buffer
|
||||||
|
this.pos = 0;
|
||||||
|
this.curFlags = 0;
|
||||||
|
this.posBegin = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// /STROBE128
|
||||||
|
// Merlin
|
||||||
|
// https://merlin.cool/index.html
|
||||||
|
class Merlin {
|
||||||
|
constructor(label) {
|
||||||
|
this.strobe = new Strobe128('Merlin v1.0');
|
||||||
|
this.appendMessage('dom-sep', label);
|
||||||
|
}
|
||||||
|
appendMessage(label, message) {
|
||||||
|
this.strobe.metaAD(label, false);
|
||||||
|
checkU32('Merlin.appendMessage', message.length);
|
||||||
|
this.strobe.metaAD((0, utils_js_1.numberToBytesLE)(message.length, 4), true);
|
||||||
|
this.strobe.AD(message, false);
|
||||||
|
}
|
||||||
|
challengeBytes(label, len) {
|
||||||
|
this.strobe.metaAD(label, false);
|
||||||
|
checkU32('Merlin.challengeBytes', len);
|
||||||
|
this.strobe.metaAD((0, utils_js_1.numberToBytesLE)(len, 4), true);
|
||||||
|
return this.strobe.PRF(len, false);
|
||||||
|
}
|
||||||
|
clean() {
|
||||||
|
this.strobe.clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// /Merlin
|
||||||
|
// Merlin signging context/transcript (sr25519 specific stuff, Merlin and Strobe are generic (but minimal))
|
||||||
|
class SigningContext extends Merlin {
|
||||||
|
constructor(name, rng = utils_js_2.randomBytes) {
|
||||||
|
super(name);
|
||||||
|
this.rng = rng;
|
||||||
|
}
|
||||||
|
label(label) {
|
||||||
|
this.appendMessage('', label);
|
||||||
|
}
|
||||||
|
bytes(bytes) {
|
||||||
|
this.appendMessage('sign-bytes', bytes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
protoName(label) {
|
||||||
|
this.appendMessage('proto-name', label);
|
||||||
|
}
|
||||||
|
commitPoint(label, point) {
|
||||||
|
this.appendMessage(label, point.toRawBytes());
|
||||||
|
}
|
||||||
|
challengeScalar(label) {
|
||||||
|
return modN((0, utils_js_1.bytesToNumberLE)(this.challengeBytes(label, 64)));
|
||||||
|
}
|
||||||
|
witnessScalar(label, nonceSeeds = []) {
|
||||||
|
return modN((0, utils_js_1.bytesToNumberLE)(this.witnessBytes(label, 64, nonceSeeds)));
|
||||||
|
}
|
||||||
|
witnessBytes(label, len, nonceSeeds = []) {
|
||||||
|
checkU32('SigningContext.witnessBytes', len);
|
||||||
|
const strobeRng = this.strobe.clone();
|
||||||
|
for (const ns of nonceSeeds) {
|
||||||
|
strobeRng.metaAD(label, false);
|
||||||
|
checkU32('SigningContext.witnessBytes nonce length', ns.length);
|
||||||
|
strobeRng.metaAD((0, utils_js_1.numberToBytesLE)(ns.length, 4), true);
|
||||||
|
strobeRng.KEY(ns, false);
|
||||||
|
}
|
||||||
|
const random = this.rng(32);
|
||||||
|
strobeRng.metaAD('rng', false);
|
||||||
|
strobeRng.KEY(random, false);
|
||||||
|
strobeRng.metaAD((0, utils_js_1.numberToBytesLE)(len, 4), false);
|
||||||
|
return strobeRng.PRF(len, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// /Merlin signing context
|
||||||
|
const MASK = (0, utils_js_1.bitMask)(256);
|
||||||
|
// == (n * CURVE.h) % CURVE_BIT_MASK
|
||||||
|
const encodeScalar = (n) => (0, utils_js_1.numberToBytesLE)((n << _3n) & MASK, 32);
|
||||||
|
// n / CURVE.h
|
||||||
|
const decodeScalar = (n) => (0, utils_js_1.bytesToNumberLE)(n) >> _3n;
|
||||||
|
// NOTE: secretKey is 64 bytes (key + nonce). This required for HDKD, since key can be derived not only from seed, but from other keys.
|
||||||
|
function getPublicKey(secretKey) {
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
const scalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
return ed25519_js_1.RistrettoPoint.BASE.multiply(scalar).toRawBytes();
|
||||||
|
}
|
||||||
|
function secretFromSeed(seed) {
|
||||||
|
abytes('seed', seed, 32);
|
||||||
|
const r = (0, sha512_js_1.sha512)(seed);
|
||||||
|
// NOTE: different from ed25519
|
||||||
|
r[0] &= 248;
|
||||||
|
r[31] &= 63;
|
||||||
|
r[31] |= 64;
|
||||||
|
// this will strip upper 3 bits and lower 3 bits
|
||||||
|
const key = encodeScalar(decodeScalar(r.subarray(0, 32)));
|
||||||
|
const nonce = r.subarray(32, 64);
|
||||||
|
const res = (0, utils_js_2.concatBytes)(key, nonce);
|
||||||
|
cleanBytes(key, nonce, r);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
// Seems like ed25519 keypair? Generates keypair from other keypair in ed25519 format
|
||||||
|
// NOTE: not exported from wasm. Do we need this at all?
|
||||||
|
function fromKeypair(pair) {
|
||||||
|
abytes('keypair', pair, 96);
|
||||||
|
const sk = pair.subarray(0, 32);
|
||||||
|
const nonce = pair.subarray(32, 64);
|
||||||
|
const pubBytes = pair.subarray(64, 96);
|
||||||
|
const key = encodeScalar((0, utils_js_1.bytesToNumberLE)(sk));
|
||||||
|
const realPub = getPublicKey(pair.subarray(0, 64));
|
||||||
|
if (!(0, utils_js_1.equalBytes)(pubBytes, realPub))
|
||||||
|
throw new Error('wrong public key');
|
||||||
|
// No need to clean since subarray's
|
||||||
|
return (0, utils_js_2.concatBytes)(key, nonce, realPub);
|
||||||
|
}
|
||||||
|
// Basic sign. NOTE: context is currently constant. Please open issue if you need different one.
|
||||||
|
const BIZINIKIWI_CONTEXT = (0, utils_js_2.utf8ToBytes)('bizinikiwi');
|
||||||
|
function sign(secretKey, message, rng = utils_js_2.randomBytes) {
|
||||||
|
abytes('message', message);
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
const t = new SigningContext('SigningContext', rng);
|
||||||
|
t.label(BIZINIKIWI_CONTEXT);
|
||||||
|
t.bytes(message);
|
||||||
|
const keyScalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
const nonce = secretKey.subarray(32, 64);
|
||||||
|
const pubPoint = ed25519_js_1.RistrettoPoint.fromHex(getPublicKey(secretKey));
|
||||||
|
t.protoName('Schnorr-sig');
|
||||||
|
t.commitPoint('sign:pk', pubPoint);
|
||||||
|
const r = t.witnessScalar('signing', [nonce]);
|
||||||
|
const R = ed25519_js_1.RistrettoPoint.BASE.multiply(r);
|
||||||
|
t.commitPoint('sign:R', R);
|
||||||
|
const k = t.challengeScalar('sign:c');
|
||||||
|
const s = modN(k * keyScalar + r);
|
||||||
|
const res = (0, utils_js_2.concatBytes)(R.toRawBytes(), (0, utils_js_1.numberToBytesLE)(s, 32));
|
||||||
|
res[63] |= 128; // add Schnorrkel marker
|
||||||
|
t.clean();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
function verify(message, signature, publicKey) {
|
||||||
|
abytes('message', message);
|
||||||
|
abytes('signature', signature, 64);
|
||||||
|
abytes('publicKey', publicKey, 32);
|
||||||
|
if ((signature[63] & 128) === 0)
|
||||||
|
throw new Error('Schnorrkel marker missing');
|
||||||
|
const sBytes = Uint8Array.from(signature.subarray(32, 64)); // copy before modification
|
||||||
|
sBytes[31] &= 127; // remove Schnorrkel marker
|
||||||
|
const R = ed25519_js_1.RistrettoPoint.fromHex(signature.subarray(0, 32));
|
||||||
|
const s = (0, utils_js_1.bytesToNumberLE)(sBytes);
|
||||||
|
(0, utils_js_1.aInRange)('s', s, _0n, CURVE_ORDER); // Just in case, it will be checked at multiplication later
|
||||||
|
const t = new SigningContext('SigningContext');
|
||||||
|
t.label(BIZINIKIWI_CONTEXT);
|
||||||
|
t.bytes(message);
|
||||||
|
const pubPoint = ed25519_js_1.RistrettoPoint.fromHex(publicKey);
|
||||||
|
if (pubPoint.equals(ed25519_js_1.RistrettoPoint.ZERO))
|
||||||
|
return false;
|
||||||
|
t.protoName('Schnorr-sig');
|
||||||
|
t.commitPoint('sign:pk', pubPoint);
|
||||||
|
t.commitPoint('sign:R', R);
|
||||||
|
const k = t.challengeScalar('sign:c');
|
||||||
|
const sP = ed25519_js_1.RistrettoPoint.BASE.multiply(s);
|
||||||
|
const RR = pubPoint.negate().multiply(k).add(sP);
|
||||||
|
t.clean();
|
||||||
|
cleanBytes(sBytes);
|
||||||
|
return RR.equals(R);
|
||||||
|
}
|
||||||
|
function getSharedSecret(secretKey, publicKey) {
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
abytes('publicKey', publicKey, 32);
|
||||||
|
const keyScalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
const pubPoint = ed25519_js_1.RistrettoPoint.fromHex(publicKey);
|
||||||
|
if (pubPoint.equals(ed25519_js_1.RistrettoPoint.ZERO))
|
||||||
|
throw new Error('wrong public key (infinity)');
|
||||||
|
return pubPoint.multiply(keyScalar).toRawBytes();
|
||||||
|
}
|
||||||
|
// Derive
|
||||||
|
exports.HDKD = {
|
||||||
|
secretSoft(secretKey, chainCode, rng = utils_js_2.randomBytes) {
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
abytes('chainCode', chainCode, 32);
|
||||||
|
const masterScalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
const masterNonce = secretKey.subarray(32, 64);
|
||||||
|
const pubPoint = ed25519_js_1.RistrettoPoint.fromHex(getPublicKey(secretKey));
|
||||||
|
const t = new SigningContext('SchnorrRistrettoHDKD', rng);
|
||||||
|
t.bytes(EMPTY);
|
||||||
|
t.appendMessage('chain-code', chainCode);
|
||||||
|
t.commitPoint('public-key', pubPoint);
|
||||||
|
const scalar = t.challengeScalar('HDKD-scalar');
|
||||||
|
const hdkdChainCode = t.challengeBytes('HDKD-chaincode', 32);
|
||||||
|
const nonceSeed = (0, utils_js_2.concatBytes)((0, utils_js_1.numberToBytesLE)(masterScalar, 32), masterNonce);
|
||||||
|
const nonce = t.witnessBytes('HDKD-nonce', 32, [masterNonce, nonceSeed]);
|
||||||
|
const key = encodeScalar(modN(masterScalar + scalar));
|
||||||
|
const res = (0, utils_js_2.concatBytes)(key, nonce);
|
||||||
|
cleanBytes(key, nonce, nonceSeed, hdkdChainCode);
|
||||||
|
t.clean();
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
publicSoft(publicKey, chainCode) {
|
||||||
|
abytes('publicKey', publicKey, 32);
|
||||||
|
abytes('chainCode', chainCode, 32);
|
||||||
|
const pubPoint = ed25519_js_1.RistrettoPoint.fromHex(publicKey);
|
||||||
|
const t = new SigningContext('SchnorrRistrettoHDKD');
|
||||||
|
t.bytes(EMPTY);
|
||||||
|
t.appendMessage('chain-code', chainCode);
|
||||||
|
t.commitPoint('public-key', pubPoint);
|
||||||
|
const scalar = t.challengeScalar('HDKD-scalar');
|
||||||
|
t.challengeBytes('HDKD-chaincode', 32);
|
||||||
|
t.clean();
|
||||||
|
return pubPoint.add(ed25519_js_1.RistrettoPoint.BASE.multiply(scalar)).toRawBytes();
|
||||||
|
},
|
||||||
|
secretHard(secretKey, chainCode) {
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
abytes('chainCode', chainCode, 32);
|
||||||
|
const key = (0, utils_js_1.numberToBytesLE)(decodeScalar(secretKey.subarray(0, 32)), 32);
|
||||||
|
const t = new SigningContext('SchnorrRistrettoHDKD');
|
||||||
|
t.bytes(EMPTY);
|
||||||
|
t.appendMessage('chain-code', chainCode);
|
||||||
|
t.appendMessage('secret-key', key);
|
||||||
|
const msk = t.challengeBytes('HDKD-hard', 32);
|
||||||
|
const hdkdChainCode = t.challengeBytes('HDKD-chaincode', 32);
|
||||||
|
t.clean();
|
||||||
|
const res = secretFromSeed(msk);
|
||||||
|
cleanBytes(key, msk, hdkdChainCode);
|
||||||
|
t.clean();
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dleq = {
|
||||||
|
proove(keyScalar, nonce, pubPoint, t, input, output) {
|
||||||
|
t.protoName('DLEQProof');
|
||||||
|
t.commitPoint('vrf:h', input);
|
||||||
|
const r = t.witnessScalar(`proving${'\0'}0`, [nonce]);
|
||||||
|
const R = ed25519_js_1.RistrettoPoint.BASE.multiply(r);
|
||||||
|
t.commitPoint('vrf:R=g^r', R);
|
||||||
|
const Hr = input.multiply(r);
|
||||||
|
t.commitPoint('vrf:h^r', Hr);
|
||||||
|
t.commitPoint('vrf:pk', pubPoint);
|
||||||
|
t.commitPoint('vrf:h^sk', output);
|
||||||
|
const c = t.challengeScalar('prove');
|
||||||
|
const s = modN(r - c * keyScalar);
|
||||||
|
return { proof: { c, s }, proofBatchable: { R, Hr, s } };
|
||||||
|
},
|
||||||
|
verify(pubPoint, t, input, output, proof) {
|
||||||
|
if (pubPoint.equals(ed25519_js_1.RistrettoPoint.ZERO))
|
||||||
|
return false;
|
||||||
|
t.protoName('DLEQProof');
|
||||||
|
t.commitPoint('vrf:h', input);
|
||||||
|
const R = pubPoint.multiply(proof.c).add(ed25519_js_1.RistrettoPoint.BASE.multiply(proof.s));
|
||||||
|
t.commitPoint('vrf:R=g^r', R);
|
||||||
|
const Hr = output.multiply(proof.c).add(input.multiply(proof.s));
|
||||||
|
t.commitPoint('vrf:h^r', Hr);
|
||||||
|
t.commitPoint('vrf:pk', pubPoint);
|
||||||
|
t.commitPoint('vrf:h^sk', output);
|
||||||
|
const realC = t.challengeScalar('prove');
|
||||||
|
if (proof.c === realC)
|
||||||
|
return { R, Hr, s: proof.s }; // proofBatchable
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// VRF: Verifiable Random Function
|
||||||
|
function initVRF(ctx, msg, extra, pubPoint, rng = utils_js_2.randomBytes) {
|
||||||
|
const t = new SigningContext('SigningContext', rng);
|
||||||
|
t.label(ctx);
|
||||||
|
t.bytes(msg);
|
||||||
|
t.commitPoint('vrf-nm-pk', pubPoint);
|
||||||
|
const hash = t.challengeBytes('VRFHash', 64);
|
||||||
|
const input = ed25519_js_1.RistrettoPoint.hashToCurve(hash);
|
||||||
|
const transcript = new SigningContext('VRF', rng);
|
||||||
|
if (extra.length)
|
||||||
|
transcript.label(extra);
|
||||||
|
t.clean();
|
||||||
|
cleanBytes(hash);
|
||||||
|
return { input, t: transcript };
|
||||||
|
}
|
||||||
|
exports.vrf = {
|
||||||
|
sign(msg, secretKey, ctx = EMPTY, extra = EMPTY, rng = utils_js_2.randomBytes) {
|
||||||
|
abytes('msg', msg);
|
||||||
|
abytes('secretKey', secretKey, 64);
|
||||||
|
abytes('ctx', ctx);
|
||||||
|
abytes('extra', extra);
|
||||||
|
const keyScalar = decodeScalar(secretKey.subarray(0, 32));
|
||||||
|
const nonce = secretKey.subarray(32, 64);
|
||||||
|
const pubPoint = ed25519_js_1.RistrettoPoint.fromHex(getPublicKey(secretKey));
|
||||||
|
const { input, t } = initVRF(ctx, msg, extra, pubPoint, rng);
|
||||||
|
const output = input.multiply(keyScalar);
|
||||||
|
const p = { input, output };
|
||||||
|
const { proof } = dleq.proove(keyScalar, nonce, pubPoint, t, input, output);
|
||||||
|
const cBytes = (0, utils_js_1.numberToBytesLE)(proof.c, 32);
|
||||||
|
const sBytes = (0, utils_js_1.numberToBytesLE)(proof.s, 32);
|
||||||
|
const res = (0, utils_js_2.concatBytes)(p.output.toRawBytes(), cBytes, sBytes);
|
||||||
|
cleanBytes(nonce, cBytes, sBytes);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
verify(msg, signature, publicKey, ctx = EMPTY, extra = EMPTY, rng = utils_js_2.randomBytes) {
|
||||||
|
abytes('msg', msg);
|
||||||
|
abytes('signature', signature, 96); // O(point) || c(scalar) || s(scalar)
|
||||||
|
abytes('pubkey', publicKey, 32);
|
||||||
|
abytes('ctx', ctx);
|
||||||
|
abytes('extra', extra);
|
||||||
|
const pubPoint = ed25519_js_1.RistrettoPoint.fromHex(publicKey);
|
||||||
|
if (pubPoint.equals(ed25519_js_1.RistrettoPoint.ZERO))
|
||||||
|
return false;
|
||||||
|
const proof = {
|
||||||
|
c: parseScalar('signature.c', signature.subarray(32, 64)),
|
||||||
|
s: parseScalar('signature.s', signature.subarray(64, 96)),
|
||||||
|
};
|
||||||
|
const { input, t } = initVRF(ctx, msg, extra, pubPoint, rng);
|
||||||
|
const output = ed25519_js_1.RistrettoPoint.fromHex(signature.subarray(0, 32));
|
||||||
|
if (output.equals(ed25519_js_1.RistrettoPoint.ZERO))
|
||||||
|
throw new Error('vrf.verify: wrong output point (identity)');
|
||||||
|
const proofBatchable = dleq.verify(pubPoint, t, input, output, proof);
|
||||||
|
return proofBatchable === false ? false : true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// NOTE: for tests only, don't use
|
||||||
|
exports.__tests = {
|
||||||
|
Strobe128,
|
||||||
|
Merlin,
|
||||||
|
SigningContext,
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@pezkuwi/scure-sr25519",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "SR25519 cryptography for PezkuwiChain with bizinikiwi signing context",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"module": "lib/esm/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./lib/esm/index.js",
|
||||||
|
"require": "./lib/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "~1.9.2",
|
||||||
|
"@noble/hashes": "~1.8.0"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/pezkuwichain/pezkuwi-scure-sr25519"
|
||||||
|
},
|
||||||
|
"author": "PezkuwiChain",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user