UNPKG

@quip.network/hashsigs

Version:
269 lines (268 loc) 11.4 kB
// 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 };