Árboles Merkle
Cómo se calcula `rules_root` y la raíz del registro, con un ejemplo que puedes recalcular.
Reglas: rules_root
Los ficheros se ordenan por ruta (bytes UTF-8). La hoja de cada fichero es SHA-256(0x00 || ruta || 0x00 || sha256_hex) y los nodos internos SHA-256(0x01 || izquierda || derecha). Un árbol de n hojas se parte en la mayor potencia de dos menor que n (RFC 6962 / RFC 9162). La raíz se guarda en el manifiesto firmado.
Dibujando el diagrama…
flowchart BT L0["leaf 0<br/>H(00 || path0 0 sha0)"]:::sec L1["leaf 1"]:::sec L2["leaf 2"]:::sec L3["leaf 3"]:::sec L4["leaf 4"]:::sec N01["node<br/>H(01 || leaf0 || leaf1)"]:::val N23["node<br/>H(01 || leaf2 || leaf3)"]:::val N03["node<br/>H(01 || N01 || N23)"]:::val R["rules_root<br/>H(01 || N03 || leaf4)"]:::gate M["manifest.json<br/>signed with Sigstore"]:::pub L0 --> N01 L1 --> N01 L2 --> N23 L3 --> N23 N01 --> N03 N23 --> N03 N03 --> R L4 --> R R --> M classDef src fill:#cfe3ff,stroke:#2f6df0,color:#0b1b33 classDef val fill:#ffe9b8,stroke:#b8860b,color:#2b1d00 classDef sec fill:#cdeedd,stroke:#1b6e3c,color:#08210f classDef pub fill:#e4d7ff,stroke:#6a3fd1,color:#1b0b3b classDef qua fill:#ffd4cc,stroke:#c0392b,color:#33100b classDef gate fill:#ffffff,stroke:#111111,stroke-width:3px,color:#111111
Descripción del diagrama
Los ficheros se ordenan por ruta. Cada hoja es el SHA-256 del byte 0x00 seguido de la ruta, un byte cero y el SHA-256 del fichero en hexadecimal. Con cinco hojas el árbol se parte en la mayor potencia de dos menor que cinco: cuatro hojas a la izquierda y una a la derecha. Los nodos internos son el SHA-256 de 0x01, izquierda y derecha. El hash superior es rules_root, guardado en el manifiesto firmado.
Código Mermaid
flowchart BT
L0["leaf 0<br/>H(00 || path0 0 sha0)"]:::sec
L1["leaf 1"]:::sec
L2["leaf 2"]:::sec
L3["leaf 3"]:::sec
L4["leaf 4"]:::sec
N01["node<br/>H(01 || leaf0 || leaf1)"]:::val
N23["node<br/>H(01 || leaf2 || leaf3)"]:::val
N03["node<br/>H(01 || N01 || N23)"]:::val
R["rules_root<br/>H(01 || N03 || leaf4)"]:::gate
M["manifest.json<br/>signed with Sigstore"]:::pub
L0 --> N01
L1 --> N01
L2 --> N23
L3 --> N23
N01 --> N03
N23 --> N03
N03 --> R
L4 --> R
R --> M
classDef src fill:#cfe3ff,stroke:#2f6df0,color:#0b1b33
classDef val fill:#ffe9b8,stroke:#b8860b,color:#2b1d00
classDef sec fill:#cdeedd,stroke:#1b6e3c,color:#08210f
classDef pub fill:#e4d7ff,stroke:#6a3fd1,color:#1b0b3b
classDef qua fill:#ffd4cc,stroke:#c0392b,color:#33100b
classDef gate fill:#ffffff,stroke:#111111,stroke-width:3px,color:#111111Prueba a recalcularlo tú con el manifest.json de la release (solo biblioteca estándar de Python). Debe imprimir True:
import hashlib, json
m = json.load(open('manifest.json'))
H = hashlib.sha256
leaf = lambda b: H(b'\x00' + b).digest()
node = lambda l, r: H(b'\x01' + l + r).digest()
def mth(xs):
if len(xs) == 1: return xs[0]
k = 1
while k * 2 < len(xs): k *= 2
return node(mth(xs[:k]), mth(xs[k:]))
files = sorted(m['files'], key=lambda f: f['path'].encode())
root = mth([leaf(f"{f['path']}\0{f['sha256']}".encode()) for f in files]).hex()
print(root, root == m['rules_root'])Efecto práctico: añadir, quitar o modificar un solo byte de un fichero, o añadir un fichero extra al paquete, cambia rules_root y la verificación falla.
Registro: raíz del log
Cada línea de logs/log.jsonl es una hoja: SHA-256(0x00 || bytes exactos de la línea). Se combinan con la misma regla. La raíz y el número de entradas forman el checkpoint (ver Registro y checkpoints).
Dibujando el diagrama…
flowchart BT E0["entry 0<br/>log.jsonl line"]:::src E1["entry 1"]:::src E2["entry 2"]:::src E3["entry 3"]:::src E4["entry 4<br/>the release entry"]:::pub E5["entry 5"]:::src A["node A = H(01 || L0 || L1)"]:::val B["node B = H(01 || L2 || L3)"]:::val C["node C = H(01 || L4 || L5)"]:::val AB["node AB"]:::val ROOT["log root"]:::gate CP["checkpoint<br/>origin, size, root"]:::sec E0 -->|leaf hash| A E1 --> A E2 --> B E3 --> B E4 -->|leaf hash| C E5 --> C A --> AB B --> AB AB --> ROOT C --> ROOT ROOT --> CP classDef src fill:#cfe3ff,stroke:#2f6df0,color:#0b1b33 classDef val fill:#ffe9b8,stroke:#b8860b,color:#2b1d00 classDef sec fill:#cdeedd,stroke:#1b6e3c,color:#08210f classDef pub fill:#e4d7ff,stroke:#6a3fd1,color:#1b0b3b classDef qua fill:#ffd4cc,stroke:#c0392b,color:#33100b classDef gate fill:#ffffff,stroke:#111111,stroke-width:3px,color:#111111
Descripción del diagrama
Cada línea de logs/log.jsonl es una hoja: SHA-256 de 0x00 seguido de los bytes exactos de la línea. Las hojas se combinan con hashes de nodo con prefijo 0x01 hasta una raíz. La raíz y el número de entradas se escriben en un checkpoint y el checkpoint se firma. La prueba de inclusión de la entrada 4 son los hashes hermanos a lo largo de su camino.
Código Mermaid
flowchart BT
E0["entry 0<br/>log.jsonl line"]:::src
E1["entry 1"]:::src
E2["entry 2"]:::src
E3["entry 3"]:::src
E4["entry 4<br/>the release entry"]:::pub
E5["entry 5"]:::src
A["node A = H(01 || L0 || L1)"]:::val
B["node B = H(01 || L2 || L3)"]:::val
C["node C = H(01 || L4 || L5)"]:::val
AB["node AB"]:::val
ROOT["log root"]:::gate
CP["checkpoint<br/>origin, size, root"]:::sec
E0 -->|leaf hash| A
E1 --> A
E2 --> B
E3 --> B
E4 -->|leaf hash| C
E5 --> C
A --> AB
B --> AB
AB --> ROOT
C --> ROOT
ROOT --> CP
classDef src fill:#cfe3ff,stroke:#2f6df0,color:#0b1b33
classDef val fill:#ffe9b8,stroke:#b8860b,color:#2b1d00
classDef sec fill:#cdeedd,stroke:#1b6e3c,color:#08210f
classDef pub fill:#e4d7ff,stroke:#6a3fd1,color:#1b0b3b
classDef qua fill:#ffd4cc,stroke:#c0392b,color:#33100b
classDef gate fill:#ffffff,stroke:#111111,stroke-width:3px,color:#111111Pruebas
- Inclusión:
inclusion-proof.jsonlleva los hashes hermanos que unen la entrada del registro de esta release con la raíz del checkpoint. - Consistencia:
consistency.jsondemuestra que el registro del tamaño anterior es prefijo del actual (solo se añadió).
LÍMITE Las páginas web recalculan estas cuentas en el navegador, pero no verifican firmas Sigstore. Para eso usa cosign, el verificador público o la app.