169 lines
4.7 KiB
Go
169 lines
4.7 KiB
Go
|
|
package tlog
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/ed25519"
|
||
|
|
"crypto/rand"
|
||
|
|
"encoding/hex"
|
||
|
|
"errors"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"basicstack.de/proofedmesh/services/verification-service/internal/canonical"
|
||
|
|
"basicstack.de/proofedmesh/services/verification-service/internal/dsse"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Local re-implementation of the transparency-log's in-memory Merkle
|
||
|
|
// backend. Kept here (rather than importing it) so this module stays
|
||
|
|
// free of cross-module dependencies — the whole point of the proof
|
||
|
|
// verifier is that it closes against bytes, not against the server
|
||
|
|
// that produced them.
|
||
|
|
type tree struct {
|
||
|
|
leaves [][]byte
|
||
|
|
}
|
||
|
|
|
||
|
|
func (t *tree) addEnvelope(env dsse.Envelope) (Entry, string) {
|
||
|
|
leafBytes, _ := canonical.Marshal(env)
|
||
|
|
t.leaves = append(t.leaves, leafHash(leafBytes))
|
||
|
|
idx := int64(len(t.leaves) - 1)
|
||
|
|
root := merkleTreeHash(t.leaves)
|
||
|
|
path := auditPath(t.leaves, int(idx))
|
||
|
|
hashes := make([]string, 0, len(path))
|
||
|
|
for _, h := range path {
|
||
|
|
hashes = append(hashes, "sha256:"+hex.EncodeToString(h))
|
||
|
|
}
|
||
|
|
hash := "sha256:" + canonical.SHA256Hex(leafBytes)
|
||
|
|
return Entry{
|
||
|
|
ContentHash: hash,
|
||
|
|
Envelope: env,
|
||
|
|
LogIndex: idx,
|
||
|
|
InclusionProof: InclusionProof{
|
||
|
|
TreeSize: int64(len(t.leaves)),
|
||
|
|
TreeRootHash: "sha256:" + hex.EncodeToString(root),
|
||
|
|
Hashes: hashes,
|
||
|
|
},
|
||
|
|
}, hash
|
||
|
|
}
|
||
|
|
|
||
|
|
func merkleTreeHash(leaves [][]byte) []byte {
|
||
|
|
switch len(leaves) {
|
||
|
|
case 0:
|
||
|
|
h := leafHash(nil)
|
||
|
|
return h
|
||
|
|
case 1:
|
||
|
|
return append([]byte(nil), leaves[0]...)
|
||
|
|
}
|
||
|
|
k := largestPow2LessThan(len(leaves))
|
||
|
|
return nodeHash(merkleTreeHash(leaves[:k]), merkleTreeHash(leaves[k:]))
|
||
|
|
}
|
||
|
|
|
||
|
|
func auditPath(leaves [][]byte, m int) [][]byte {
|
||
|
|
n := len(leaves)
|
||
|
|
if n <= 1 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
k := largestPow2LessThan(n)
|
||
|
|
if m < k {
|
||
|
|
return append(auditPath(leaves[:k], m), merkleTreeHash(leaves[k:]))
|
||
|
|
}
|
||
|
|
return append(auditPath(leaves[k:], m-k), merkleTreeHash(leaves[:k]))
|
||
|
|
}
|
||
|
|
|
||
|
|
func largestPow2LessThan(n int) int {
|
||
|
|
k := 1
|
||
|
|
for k < n {
|
||
|
|
k <<= 1
|
||
|
|
}
|
||
|
|
return k >> 1
|
||
|
|
}
|
||
|
|
|
||
|
|
func newEnvelope(t *testing.T, s string) dsse.Envelope {
|
||
|
|
t.Helper()
|
||
|
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("gen key: %v", err)
|
||
|
|
}
|
||
|
|
return dsse.Sign("application/vnd.in-toto+json", []byte(s), "k1", priv)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestVerifyEntry_SingleLeafTree(t *testing.T) {
|
||
|
|
var tr tree
|
||
|
|
entry, _ := tr.addEnvelope(newEnvelope(t, `{"i":0}`))
|
||
|
|
if err := VerifyEntry(entry); err != nil {
|
||
|
|
t.Fatalf("single-leaf: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestVerifyEntry_MultiLeafTree(t *testing.T) {
|
||
|
|
var tr tree
|
||
|
|
// Append five envelopes; verify every leaf's inclusion proof.
|
||
|
|
entries := make([]Entry, 0, 5)
|
||
|
|
for i := 0; i < 5; i++ {
|
||
|
|
e, _ := tr.addEnvelope(newEnvelope(t, "payload-"+string(rune('a'+i))))
|
||
|
|
entries = append(entries, e)
|
||
|
|
}
|
||
|
|
// Re-fetch the up-to-date proofs for every leaf against the final tree.
|
||
|
|
for i := range entries {
|
||
|
|
leaf := tr.leaves[i]
|
||
|
|
path := auditPath(tr.leaves, i)
|
||
|
|
hashes := make([]string, 0, len(path))
|
||
|
|
for _, h := range path {
|
||
|
|
hashes = append(hashes, "sha256:"+hex.EncodeToString(h))
|
||
|
|
}
|
||
|
|
entries[i].InclusionProof = InclusionProof{
|
||
|
|
TreeSize: int64(len(tr.leaves)),
|
||
|
|
TreeRootHash: "sha256:" + hex.EncodeToString(merkleTreeHash(tr.leaves)),
|
||
|
|
Hashes: hashes,
|
||
|
|
}
|
||
|
|
_ = leaf
|
||
|
|
if err := VerifyEntry(entries[i]); err != nil {
|
||
|
|
t.Fatalf("leaf %d: %v", i, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestVerifyEntry_TamperedRoot(t *testing.T) {
|
||
|
|
var tr tree
|
||
|
|
entry, _ := tr.addEnvelope(newEnvelope(t, `{"i":0}`))
|
||
|
|
tr.addEnvelope(newEnvelope(t, `{"i":1}`))
|
||
|
|
// Re-fetch leaf 0's proof against the 2-leaf tree.
|
||
|
|
path := auditPath(tr.leaves, 0)
|
||
|
|
hashes := make([]string, 0, len(path))
|
||
|
|
for _, h := range path {
|
||
|
|
hashes = append(hashes, "sha256:"+hex.EncodeToString(h))
|
||
|
|
}
|
||
|
|
entry.InclusionProof = InclusionProof{
|
||
|
|
TreeSize: 2,
|
||
|
|
TreeRootHash: "sha256:" + hex.EncodeToString(merkleTreeHash(tr.leaves)),
|
||
|
|
Hashes: hashes,
|
||
|
|
}
|
||
|
|
// Flip a bit in the stated root — proof must fail.
|
||
|
|
rootBytes, _ := hex.DecodeString(entry.InclusionProof.TreeRootHash[len("sha256:"):])
|
||
|
|
rootBytes[0] ^= 0xff
|
||
|
|
entry.InclusionProof.TreeRootHash = "sha256:" + hex.EncodeToString(rootBytes)
|
||
|
|
err := VerifyEntry(entry)
|
||
|
|
if !errors.Is(err, ErrInclusionProofInvalid) {
|
||
|
|
t.Fatalf("want ErrInclusionProofInvalid, got %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestVerifyEntry_ContentHashMismatch(t *testing.T) {
|
||
|
|
var tr tree
|
||
|
|
entry, _ := tr.addEnvelope(newEnvelope(t, `{"i":0}`))
|
||
|
|
// Flip a bit in the envelope payload after the proof was captured.
|
||
|
|
entry.Envelope.Payload = "aaaa"
|
||
|
|
err := VerifyEntry(entry)
|
||
|
|
if !errors.Is(err, ErrInclusionProofMalformed) {
|
||
|
|
t.Fatalf("want ErrInclusionProofMalformed, got %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestVerifyEntry_IndexOutOfRange(t *testing.T) {
|
||
|
|
var tr tree
|
||
|
|
entry, _ := tr.addEnvelope(newEnvelope(t, `{"i":0}`))
|
||
|
|
entry.LogIndex = 7
|
||
|
|
entry.InclusionProof.TreeSize = 7
|
||
|
|
err := VerifyEntry(entry)
|
||
|
|
if !errors.Is(err, ErrInclusionProofMalformed) {
|
||
|
|
t.Fatalf("want ErrInclusionProofMalformed, got %v", err)
|
||
|
|
}
|
||
|
|
}
|