sigstore._internal.merkle
Utilities for verifying proof-of-inclusion within Rekor's Merkle Tree.
This code is based off Google's Trillian Merkle Tree implementation which Cosign uses to validate Rekor entries.
The data format for the Merkle tree nodes is described in IETF's RFC 6962.
1# Copyright 2022 The Sigstore Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16Utilities for verifying proof-of-inclusion within Rekor's Merkle Tree. 17 18This code is based off Google's Trillian Merkle Tree implementation which Cosign uses to validate 19Rekor entries. 20 21The data format for the Merkle tree nodes is described in IETF's RFC 6962. 22""" 23 24from __future__ import annotations 25 26import base64 27import hashlib 28import struct 29import typing 30from typing import List, Tuple 31 32from sigstore._utils import HexStr 33from sigstore.errors import VerificationError 34 35if typing.TYPE_CHECKING: 36 from sigstore.models import LogEntry 37 38 39_LEAF_HASH_PREFIX = 0 40_NODE_HASH_PREFIX = 1 41 42 43def _decomp_inclusion_proof(index: int, size: int) -> Tuple[int, int]: 44 """ 45 Breaks down inclusion proof for a leaf at the specified |index| in a tree of the specified 46 |size| into 2 components. The splitting point between them is where paths to leaves |index| and 47 |size-1| diverge. 48 49 Returns lengths of the bottom and upper proof parts correspondingly. The sum of the two 50 determines the correct length of the inclusion proof. 51 """ 52 53 inner = (index ^ (size - 1)).bit_length() 54 border = bin(index >> inner).count("1") 55 return inner, border 56 57 58def _chain_inner(seed: bytes, hashes: List[str], log_index: int) -> bytes: 59 """ 60 Computes a subtree hash for a node on or below the tree's right border. Assumes |proof| hashes 61 are ordered from lower levels to upper, and |seed| is the initial subtree/leaf hash on the path 62 located at the specified |index| on its level. 63 """ 64 65 for i in range(len(hashes)): 66 h = bytes.fromhex(hashes[i]) 67 if (log_index >> i) & 1 == 0: 68 seed = _hash_children(seed, h) 69 else: 70 seed = _hash_children(h, seed) 71 return seed 72 73 74def _chain_border_right(seed: bytes, hashes: List[str]) -> bytes: 75 """ 76 Chains proof hashes along tree borders. This differs from inner chaining because |proof| 77 contains only left-side subtree hashes. 78 """ 79 80 for h in hashes: 81 seed = _hash_children(bytes.fromhex(h), seed) 82 return seed 83 84 85def _hash_children(lhs: bytes, rhs: bytes) -> bytes: 86 pattern = f"B{len(lhs)}s{len(rhs)}s" 87 data = struct.pack(pattern, _NODE_HASH_PREFIX, lhs, rhs) 88 return hashlib.sha256(data).digest() 89 90 91def _hash_leaf(leaf: bytes) -> bytes: 92 pattern = f"B{len(leaf)}s" 93 data = struct.pack(pattern, _LEAF_HASH_PREFIX, leaf) 94 return hashlib.sha256(data).digest() 95 96 97def verify_merkle_inclusion(entry: LogEntry) -> None: 98 """Verify the Merkle Inclusion Proof for a given Rekor entry.""" 99 inclusion_proof = entry.inclusion_proof 100 101 # Figure out which subset of hashes corresponds to the inner and border nodes. 102 inner, border = _decomp_inclusion_proof( 103 inclusion_proof.log_index, inclusion_proof.tree_size 104 ) 105 106 # Check against the number of hashes. 107 if len(inclusion_proof.hashes) != (inner + border): 108 raise VerificationError( 109 f"inclusion proof has wrong size: expected {inner + border}, got " 110 f"{len(inclusion_proof.hashes)}" 111 ) 112 113 # The new entry's hash isn't included in the inclusion proof so we should calculate this 114 # ourselves. 115 leaf_hash: bytes = _hash_leaf(base64.b64decode(entry.body)) 116 117 # Now chain the hashes belonging to the inner and border portions. We should expect the 118 # calculated hash to match the root hash. 119 intermediate_result: bytes = _chain_inner( 120 leaf_hash, inclusion_proof.hashes[:inner], inclusion_proof.log_index 121 ) 122 123 calc_hash: HexStr = HexStr( 124 _chain_border_right(intermediate_result, inclusion_proof.hashes[inner:]).hex() 125 ) 126 127 if calc_hash != inclusion_proof.root_hash: 128 raise VerificationError( 129 f"inclusion proof contains invalid root hash: expected {inclusion_proof}, calculated " 130 f"{calc_hash}" 131 )
98def verify_merkle_inclusion(entry: LogEntry) -> None: 99 """Verify the Merkle Inclusion Proof for a given Rekor entry.""" 100 inclusion_proof = entry.inclusion_proof 101 102 # Figure out which subset of hashes corresponds to the inner and border nodes. 103 inner, border = _decomp_inclusion_proof( 104 inclusion_proof.log_index, inclusion_proof.tree_size 105 ) 106 107 # Check against the number of hashes. 108 if len(inclusion_proof.hashes) != (inner + border): 109 raise VerificationError( 110 f"inclusion proof has wrong size: expected {inner + border}, got " 111 f"{len(inclusion_proof.hashes)}" 112 ) 113 114 # The new entry's hash isn't included in the inclusion proof so we should calculate this 115 # ourselves. 116 leaf_hash: bytes = _hash_leaf(base64.b64decode(entry.body)) 117 118 # Now chain the hashes belonging to the inner and border portions. We should expect the 119 # calculated hash to match the root hash. 120 intermediate_result: bytes = _chain_inner( 121 leaf_hash, inclusion_proof.hashes[:inner], inclusion_proof.log_index 122 ) 123 124 calc_hash: HexStr = HexStr( 125 _chain_border_right(intermediate_result, inclusion_proof.hashes[inner:]).hex() 126 ) 127 128 if calc_hash != inclusion_proof.root_hash: 129 raise VerificationError( 130 f"inclusion proof contains invalid root hash: expected {inclusion_proof}, calculated " 131 f"{calc_hash}" 132 )
Verify the Merkle Inclusion Proof for a given Rekor entry.