@quip.network/hashsigs
Version:
Hash-based signatures, WOTS+
269 lines (268 loc) • 11.4 kB
JavaScript
// src/wotsplus.ts
var BufferUtil = {
equals: (a, b) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
},
from: (data) => {
if (typeof Buffer !== "undefined") {
return Buffer.from(data);
}
return new Uint8Array(data);
}
};
var WOTSPlus = class {
hashFn;
// HashLen: The WOTS+ `n` security parameter which is the size
// of the hash function output in bytes.
// This is 32 for keccak256 (256 / 8 = 32)
hashLen;
// MessageLen: The WOTS+ `m` parameter which is the size
// of the message to be signed in bytes
// (and also the size of our hash function)
//
// This is 32 for keccak256 (256 / 8 = 32)
//
// Note that this is not the message length itself as, like
// with most signatures, we hash the message and then compute
// the signature on the hash of the message.
messageLen;
// ChainLen: The WOTS+ `w`(internitz) parameter.
// This corresponds to the number of hash chains for each public
// key segment and the base-w representation of the message
// and checksum.
//
// A larger value means a smaller signature size but a longer
// computation time.
//
// For XMSS (rfc8391) this value is limited to 4 or 16 because
// they simplify the algorithm and offer the best trade-offs.
chainLen;
// lg(ChainLen) so we don't calculate it repeatedly
lgChainLen;
// NumMessageChunks: the `len_1` parameter which is the number of
// message chunks. This is
// ceil(8n / lg(w)) -> ceil(8 * HashLen / lg(ChainLen))
// or ceil(32*8 / lg(16)) -> 256 / 4 = 64
// Python: math.ceil(32*8 / math.log(16,2))
numMessageChunks;
// NumChecksumChunks: the `len_2` parameter which is the number of
// checksum chunks. This is
// floor(lg(len_1 * (w - 1)) / lg(w)) + 1
// -> floor(lg(NumMessageChunks * (ChainLen - 1)) / lg(ChainLen)) + 1
// -> floor(lg(64 * 15) / lg(16)) + 1 = 3
// Python: math.floor(math.log(64 * 15, 2) / math.log(16, 2)) + 1
numChecksumChunks;
numSignatureChunks;
// SignatureSize: The size of the signature in bytes.
signatureSize;
// PublicKeySize: The size of the public key in bytes.
publicKeySize;
constructor(hashFunction, hashLen, chainLen = 16) {
this.hashFn = hashFunction;
this.hashLen = hashLen ?? 32;
this.messageLen = this.hashLen;
this.chainLen = chainLen;
this.lgChainLen = Math.log2(this.chainLen);
this.numMessageChunks = Math.ceil(8 * this.hashLen / this.lgChainLen);
const checksumBits = Math.floor(
Math.log2(this.numMessageChunks * (this.chainLen - 1)) / Math.log2(this.chainLen)
) + 1;
this.numChecksumChunks = checksumBits;
this.numSignatureChunks = this.numMessageChunks + this.numChecksumChunks;
this.signatureSize = this.numSignatureChunks * this.hashLen;
this.publicKeySize = this.hashLen * 2;
this.validateParameters();
}
validateParameters() {
if ((this.chainLen & this.chainLen - 1) !== 0) {
throw new Error("ChainLen must be a power of 2");
}
if (this.hashLen <= 0) {
throw new Error("HashLen must be positive");
}
if (this.chainLen !== 16 && this.chainLen !== 4) {
throw new Error("ChainLen must be either 4 or 16 for XMSS compatibility");
}
}
// Hash: The WOTS+ `F` hash function.
hash(data) {
return this.hashFn(data);
}
// prf: Generate randomization elements from seed and index
// Similar to XMSS RFC 8391 section 5.1
// NOTE: while sha256 and ripemd160 are available in solidity,
// they are implemented as precompiled contracts and are more expensive for gas.
prf(seed, index) {
const buffer = new Uint8Array(1 + seed.length + 2);
buffer[0] = 3;
buffer.set(seed, 1);
buffer[seed.length + 1] = index >> 8 & 255;
buffer[seed.length + 2] = index & 255;
return this.hash(buffer);
}
// Generate randomization elements from seed and index
// Similar to XMSS RFC 8391 section 5.1
generateRandomizationElements(publicSeed) {
const elements = [];
for (let i = 0; i < this.numSignatureChunks; i++) {
elements.push(this.prf(publicSeed, i));
}
return elements;
}
// chain is the c_k^i function,
// the hash of (prevChainOut XOR randomization element at index).
// As a practical matter, we generate the randomization elements
// via a seed like in XMSS(rfc8391) with a defined PRF.
chain(prevChainOut, randomizationElements, index, steps) {
if (index + steps >= this.chainLen) {
throw new Error("steps + index must be less than ChainLen");
}
let chainOut = prevChainOut;
for (let i = 1; i <= steps; i++) {
const xored = this.xor(chainOut, randomizationElements[i + index]);
chainOut = this.hash(xored);
}
return chainOut;
}
// xor: Bitwise XOR of two byte arrays
xor(a, b) {
if (a.length !== b.length) {
throw new Error("Arrays must have equal length");
}
const result = new Uint8Array(a.length);
for (let i = 0; i < a.length; i++) {
result[i] = a[i] ^ b[i];
}
return result;
}
// Generate key pair
generateKeyPair(privateSeed, publicSeed) {
const combinedSeed = new Uint8Array([...privateSeed, ...publicSeed]);
const privateKey = this.hash(combinedSeed);
const randomizationElements = this.generateRandomizationElements(publicSeed);
const functionKey = randomizationElements[0];
const publicKeySegments = new Uint8Array(this.numSignatureChunks * this.hashLen);
for (let i = 0; i < this.numSignatureChunks; i++) {
const secretKeySegment = this.hash(
new Uint8Array([...functionKey, ...this.prf(privateKey, i + 1)])
);
const segment = this.chain(secretKeySegment, randomizationElements, 0, this.chainLen - 1);
publicKeySegments.set(segment, i * this.hashLen);
}
const publicKeyHash = this.hash(publicKeySegments);
const publicKey = new Uint8Array([...publicSeed, ...publicKeyHash]);
return { publicKey, privateKey };
}
// sign: Sign a message with a WOTS+ private key.
sign(privateKey, publicSeed, message) {
if (privateKey.length !== this.hashLen) {
throw new Error(`private key length must be ${this.hashLen} bytes`);
}
if (message.length !== this.messageLen) {
throw new Error(`message length must be ${this.messageLen} bytes`);
}
const randomizationElements = this.generateRandomizationElements(publicSeed);
const functionKey = randomizationElements[0];
const signature = new Array(this.numSignatureChunks);
const chainSegments = this.computeMessageHashChainIndexes(message);
for (let i = 0; i < chainSegments.length; i++) {
const chainIdx = chainSegments[i];
const secretKeySegment = this.hash(
new Uint8Array([...functionKey, ...this.prf(privateKey, i + 1)])
);
signature[i] = this.chain(secretKeySegment, randomizationElements, 0, chainIdx);
}
return signature;
}
// verify: Verify a WOTS+ signature.
// 1. The first part of the publicKey is a public seed used to regenerate the randomization elements. (`r` from the paper).
// 2. The second part of the publicKey is the hash of the NumMessageChunks + NumChecksumChunks public key segments.
// 3. Convert the Message to "base-w" representation (or base of ChainLen representation).
// 4. Compute and add the checksum.
// 5. Run the chain function on each segment to reproduce each public key segment.
// 6. Hash all public key segments together to recreate the original public key.
verify(publicKey, message, signature) {
if (publicKey.length !== this.publicKeySize) {
throw new Error(`public key length must be ${this.publicKeySize} bytes`);
}
const publicSeed = publicKey.slice(0, this.hashLen);
const publicKeyHash = publicKey.slice(this.hashLen, this.publicKeySize);
const randomizationElements = this.generateRandomizationElements(publicSeed);
return this.verifyWithRandomizationElements(
publicKeyHash,
message,
signature,
randomizationElements
);
}
// verify: Verify a WOTS+ signature.
// 1. The first part of the publicKey is a public seed used to regenerate the randomization elements. (`r` from the paper).
// 2. The second part of the publicKey is the hash of the NumMessageChunks + NumChecksumChunks public key segments.
// 3. Convert the Message to "base-w" representation (or base of ChainLen representation).
// 4. Compute and add the checksum.
// 5. Run the chain function on each segment to reproduce each public key segment.
// 6. Hash all public key segments together to recreate the original public key.
verifyWithRandomizationElements(publicKeyHash, message, signature, randomizationElements) {
if (publicKeyHash.length !== this.hashLen) {
throw new Error(`public key hash length must be ${this.hashLen} bytes`);
}
if (message.length !== this.messageLen) {
throw new Error(`message length must be ${this.messageLen} bytes`);
}
if (signature.length !== this.numSignatureChunks) {
throw new Error(`signature length must be ${this.numSignatureChunks}`);
}
const chainSegments = this.computeMessageHashChainIndexes(message);
const publicKeySegments = new Uint8Array(this.numSignatureChunks * this.hashLen);
for (let i = 0; i < chainSegments.length; i++) {
const chainIdx = chainSegments[i];
const numIterations = this.chainLen - chainIdx - 1;
const prevChainOut = signature[i];
const segment = this.chain(prevChainOut, randomizationElements, chainIdx, numIterations);
publicKeySegments.set(segment, i * this.hashLen);
}
const computedHash = this.hash(publicKeySegments);
return BufferUtil.equals(computedHash, publicKeyHash);
}
// toBaseW: Convert a message to base-w representation (or base of ChainLen representation)
// These numbers are used to index into each hash chain which is rooted at a secret key segment and produces
// a public key segment at the end of the chain. Verification of a signature means using these
// index into each hash chain to recompute the corresponding public key segment.
toBaseW(message, numChunks, basew, offset) {
let index = 0;
for (let i = 0; i < numChunks; i++) {
if (i % 2 === 0) {
basew[offset + i] = message[index] >> 4 & 15;
} else {
basew[offset + i] = message[index] & 15;
index++;
}
}
}
// Compute checksum for the chain indexes
checksum(chainIndexes) {
let sum = 0;
for (let i = 0; i < this.numMessageChunks; i++) {
sum += this.chainLen - 1 - chainIndexes[i];
}
for (let i = 0; i < this.numChecksumChunks; i++) {
chainIndexes[this.numMessageChunks + i] = sum >> (this.numChecksumChunks - 1 - i) * this.lgChainLen & this.chainLen - 1;
}
}
// Compute message hash chain indexes
// We convert the message to base-w representation (or base of ChainLen representation)
// We attach the checksum, also in base-w representation, to the end of the hash chain index list.
computeMessageHashChainIndexes(message) {
const chainIndexes = new Array(this.numMessageChunks + this.numChecksumChunks).fill(0);
this.toBaseW(message, this.numMessageChunks, chainIndexes, 0);
this.checksum(chainIndexes);
return chainIndexes;
}
};
export {
WOTSPlus
};