|
| 1 | +"use strict"; |
| 2 | +var __importDefault = (this && this.__importDefault) || function (mod) { |
| 3 | + return (mod && mod.__esModule) ? mod : { "default": mod }; |
| 4 | +}; |
| 5 | +Object.defineProperty(exports, "__esModule", { value: true }); |
| 6 | +exports.verifyMerkleInclusion = void 0; |
| 7 | +/* |
| 8 | +Copyright 2023 The Sigstore Authors. |
| 9 | +
|
| 10 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 11 | +you may not use this file except in compliance with the License. |
| 12 | +You may obtain a copy of the License at |
| 13 | +
|
| 14 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 15 | +
|
| 16 | +Unless required by applicable law or agreed to in writing, software |
| 17 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 18 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 19 | +See the License for the specific language governing permissions and |
| 20 | +limitations under the License. |
| 21 | +*/ |
| 22 | +const crypto_1 = __importDefault(require("crypto")); |
| 23 | +const error_1 = require("../../error"); |
| 24 | +const RFC6962_LEAF_HASH_PREFIX = Buffer.from([0x00]); |
| 25 | +const RFC6962_NODE_HASH_PREFIX = Buffer.from([0x01]); |
| 26 | +function verifyMerkleInclusion(entry) { |
| 27 | + const inclusionProof = entry.inclusionProof; |
| 28 | + if (!inclusionProof) { |
| 29 | + throw new error_1.VerificationError('tlog entry has no inclusion proof'); |
| 30 | + } |
| 31 | + const logIndex = BigInt(inclusionProof.logIndex); |
| 32 | + const treeSize = BigInt(inclusionProof.treeSize); |
| 33 | + if (logIndex < 0n || logIndex >= treeSize) { |
| 34 | + throw new error_1.VerificationError('invalid inclusion proof index'); |
| 35 | + } |
| 36 | + // Figure out which subset of hashes corresponds to the inner and border |
| 37 | + // nodes |
| 38 | + const { inner, border } = decompInclProof(logIndex, treeSize); |
| 39 | + if (inclusionProof.hashes.length !== inner + border) { |
| 40 | + throw new error_1.VerificationError('invalid inclusion proof length'); |
| 41 | + } |
| 42 | + const innerHashes = inclusionProof.hashes.slice(0, inner); |
| 43 | + const borderHashes = inclusionProof.hashes.slice(inner); |
| 44 | + // The entry's hash is the leaf hash |
| 45 | + const leafHash = hashLeaf(entry.canonicalizedBody); |
| 46 | + // Chain the hashes belonging to the inner and border portions |
| 47 | + const calculatedHash = chainBorderRight(chainInner(leafHash, innerHashes, logIndex), borderHashes); |
| 48 | + // Calculated hash should match the root hash in the inclusion proof |
| 49 | + return bufferEqual(calculatedHash, inclusionProof.rootHash); |
| 50 | +} |
| 51 | +exports.verifyMerkleInclusion = verifyMerkleInclusion; |
| 52 | +// Breaks down inclusion proof for a leaf at the specified index in a tree of |
| 53 | +// the specified size. The split point is where paths to the index leaf and |
| 54 | +// the (size - 1) leaf diverge. Returns lengths of the bottom and upper proof |
| 55 | +// parts. |
| 56 | +function decompInclProof(index, size) { |
| 57 | + const inner = innerProofSize(index, size); |
| 58 | + const border = onesCount(index >> BigInt(inner)); |
| 59 | + return { inner, border }; |
| 60 | +} |
| 61 | +// Computes a subtree hash for a node on or below the tree's right border. |
| 62 | +// Assumes the provided proof hashes are ordered from lower to higher levels |
| 63 | +// and seed is the initial hash of the node specified by the index. |
| 64 | +function chainInner(seed, hashes, index) { |
| 65 | + return hashes.reduce((acc, h, i) => { |
| 66 | + if ((index >> BigInt(i)) & BigInt(1)) { |
| 67 | + return hashChildren(h, acc); |
| 68 | + } |
| 69 | + else { |
| 70 | + return hashChildren(acc, h); |
| 71 | + } |
| 72 | + }, seed); |
| 73 | +} |
| 74 | +// Computes a subtree hash for nodes along the tree's right border. |
| 75 | +function chainBorderRight(seed, hashes) { |
| 76 | + return hashes.reduce((acc, h) => hashChildren(h, acc), seed); |
| 77 | +} |
| 78 | +function innerProofSize(index, size) { |
| 79 | + return (index ^ (size - BigInt(1))).toString(2).length; |
| 80 | +} |
| 81 | +// Counts the number of ones in the binary representation of the given number. |
| 82 | +// https://en.wikipedia.org/wiki/Hamming_weight |
| 83 | +function onesCount(x) { |
| 84 | + return x.toString(2).split('1').length - 1; |
| 85 | +} |
| 86 | +// Hashing logic according to RFC6962. |
| 87 | +// https://datatracker.ietf.org/doc/html/rfc6962#section-2 |
| 88 | +function hashChildren(left, right) { |
| 89 | + const hasher = crypto_1.default.createHash('sha256'); |
| 90 | + hasher.update(RFC6962_NODE_HASH_PREFIX); |
| 91 | + hasher.update(left); |
| 92 | + hasher.update(right); |
| 93 | + return hasher.digest(); |
| 94 | +} |
| 95 | +function hashLeaf(leaf) { |
| 96 | + const hasher = crypto_1.default.createHash('sha256'); |
| 97 | + hasher.update(RFC6962_LEAF_HASH_PREFIX); |
| 98 | + hasher.update(leaf); |
| 99 | + return hasher.digest(); |
| 100 | +} |
| 101 | +function bufferEqual(a, b) { |
| 102 | + try { |
| 103 | + return crypto_1.default.timingSafeEqual(a, b); |
| 104 | + } |
| 105 | + catch { |
| 106 | + /* istanbul ignore next */ |
| 107 | + return false; |
| 108 | + } |
| 109 | +} |
0 commit comments