Skip to content

Commit b3c23cf

Browse files
committed
ctlog: fix a data race and expand sequencer tests
1 parent c44718b commit b3c23cf

File tree

5 files changed

+358
-12
lines changed

5 files changed

+358
-12
lines changed

.github/workflows/test.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ jobs:
2020
go-version-file: go.mod
2121
check-latest: true
2222
- name: Run tests
23-
run: go test -race ./...
23+
run: go test ./...
24+
- name: Run tests (short + race)
25+
run: go test -short -race ./...

internal/ctlog/ctlog.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,9 @@ func (l *Log) sequencePool() error {
209209
if n%tileWidth == 0 { // Data tile is full.
210210
tile := tlog.TileForIndex(tileHeight, tlog.StoredHashIndex(0, n-1))
211211
tile.L = -1
212-
edgeTiles[-1] = tileWithBytes{tile, dataTile}
213-
g.Go(func() error { return l.backend.Upload(gctx, tile.Path(), dataTile) })
212+
data := dataTile
213+
edgeTiles[-1] = tileWithBytes{tile, data}
214+
g.Go(func() error { return l.backend.Upload(gctx, tile.Path(), data) })
214215
dataTile = nil
215216
}
216217
}
@@ -269,13 +270,12 @@ func (l *Log) sequencePool() error {
269270
// signTreeHead signs the tree and returns a checkpoint according to
270271
// c2sp.org/checkpoint.
271272
func (l *Log) signTreeHead(tree tlog.Tree, timestamp int64) (checkpoint []byte, err error) {
272-
sth := &ct.SignedTreeHead{
273+
sthBytes, err := ct.SerializeSTHSignatureInput(ct.SignedTreeHead{
273274
Version: ct.V1,
274275
TreeSize: uint64(tree.N),
275276
Timestamp: uint64(timestamp),
276277
SHA256RootHash: ct.SHA256Hash(tree.Hash),
277-
}
278-
sthBytes, err := ct.SerializeSTHSignatureInput(*sth)
278+
})
279279
if err != nil {
280280
return nil, err
281281
}
@@ -342,7 +342,7 @@ func digitallySign(k crypto.Signer, msg []byte) ([]byte, error) {
342342

343343
func (l *Log) hashReader(overlay map[int64]tlog.Hash) tlog.HashReaderFunc {
344344
return func(indexes []int64) ([]tlog.Hash, error) {
345-
var list []tlog.Hash
345+
list := make([]tlog.Hash, 0, len(indexes))
346346
for _, id := range indexes {
347347
if h, ok := overlay[id]; ok {
348348
list = append(list, h)

internal/ctlog/ctlog_test.go

+86-4
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,100 @@ package ctlog_test
22

33
import (
44
"bytes"
5+
"crypto/rand"
6+
"flag"
7+
mathrand "math/rand"
58
"testing"
9+
"time"
610

711
"filippo.io/litetlog/internal/ctlog/cttest"
812
)
913

10-
func TestSequencer(t *testing.T) {
14+
var longFlag = flag.Bool("long", false, "run especially slow tests")
15+
16+
func TestSequenceOneLeaf(t *testing.T) {
1117
tl := cttest.NewEmptyTestLog(t)
12-
id := tl.Log.AddCertificate([]byte("AAA"))
18+
19+
n := int64(1024 + 2)
20+
if *longFlag {
21+
n *= 1024
22+
}
23+
if testing.Short() {
24+
n = 3
25+
}
26+
for i := int64(0); i < n; i++ {
27+
cert := make([]byte, mathrand.Intn(4)+1)
28+
rand.Read(cert)
29+
30+
id := tl.Log.AddCertificate(cert)
31+
fatalIfErr(t, tl.Log.Sequence())
32+
if id := id(); id != i {
33+
t.Errorf("got leaf index %d, expected %d", id, 0)
34+
}
35+
36+
if !*longFlag {
37+
tl.CheckLog()
38+
// TODO: check leaf contents at index id.
39+
}
40+
}
41+
tl.CheckLog()
42+
}
43+
44+
func TestSequenceLargeLog(t *testing.T) {
45+
if testing.Short() {
46+
t.Skip()
47+
}
48+
49+
tl := cttest.NewEmptyTestLog(t)
50+
for i := 0; i < 5; i++ {
51+
cert := make([]byte, mathrand.Intn(4)+1)
52+
rand.Read(cert)
53+
tl.Log.AddCertificate(cert)
54+
}
1355
fatalIfErr(t, tl.Log.Sequence())
14-
if id := id(); id != 0 {
15-
t.Errorf("got leaf index %d, expected %d", id, 0)
56+
tl.CheckLog()
57+
58+
for i := 0; i < 500; i++ {
59+
for i := 0; i < 3000; i++ {
60+
cert := make([]byte, mathrand.Intn(4)+1)
61+
rand.Read(cert)
62+
tl.Log.AddCertificate(cert)
63+
}
64+
fatalIfErr(t, tl.Log.Sequence())
65+
}
66+
tl.CheckLog()
67+
}
68+
69+
func TestSequenceEmptyPool(t *testing.T) {
70+
sequenceTwice := func(tl *cttest.TestLog) {
71+
fatalIfErr(t, tl.Log.Sequence())
72+
t1 := tl.CheckLog()
73+
time.Sleep(3 * time.Millisecond)
74+
fatalIfErr(t, tl.Log.Sequence())
75+
t2 := tl.CheckLog()
76+
if t1 >= t2 {
77+
t.Helper()
78+
t.Error("time did not advance")
79+
}
80+
}
81+
addCerts := func(tl *cttest.TestLog, n int) {
82+
for i := 0; i < n; i++ {
83+
cert := make([]byte, mathrand.Intn(1000)+1)
84+
rand.Read(cert)
85+
tl.Log.AddCertificate(cert)
86+
}
1687
}
88+
89+
tl := cttest.NewEmptyTestLog(t)
90+
sequenceTwice(tl)
91+
addCerts(tl, 5) // 5
92+
sequenceTwice(tl)
93+
addCerts(tl, 1024-5-1) // 1024 - 1
94+
sequenceTwice(tl)
95+
addCerts(tl, 1) // 1024
96+
sequenceTwice(tl)
97+
addCerts(tl, 1) // 1024 + 1
98+
sequenceTwice(tl)
1799
}
18100

19101
func fatalIfErr(t testing.TB, err error) {

internal/ctlog/cttest/cttest.go

+157-1
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,24 @@ import (
55
"crypto/ecdsa"
66
"crypto/elliptic"
77
"crypto/rand"
8+
"crypto/x509"
9+
"encoding/pem"
10+
"fmt"
811
"sync"
912
"testing"
1013

1114
"filippo.io/litetlog/internal/ctlog"
15+
"filippo.io/litetlog/internal/tlogx"
16+
"golang.org/x/crypto/cryptobyte"
17+
"golang.org/x/mod/sumdb/note"
18+
"golang.org/x/mod/sumdb/tlog"
1219
)
1320

1421
type TestLog struct {
1522
Log *ctlog.Log
1623
Backend *MemoryBackend
1724
Key *ecdsa.PrivateKey
25+
t testing.TB
1826
}
1927

2028
func NewEmptyTestLog(t testing.TB) *TestLog {
@@ -23,17 +31,141 @@ func NewEmptyTestLog(t testing.TB) *TestLog {
2331
if err != nil {
2432
t.Fatal(err)
2533
}
34+
k, err := x509.MarshalPKCS8PrivateKey(key)
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
t.Logf("ECDSA key: %s", pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: k}))
2639
log, err := ctlog.NewLog("example.com/TestLog", key, backend)
2740
if err != nil {
2841
t.Fatal(err)
2942
}
30-
return &TestLog{
43+
return &TestLog{t: t,
3144
Log: log,
3245
Backend: backend,
3346
Key: key,
3447
}
3548
}
3649

50+
func (tl *TestLog) CheckLog() (sthTimestamp uint64) {
51+
t := tl.t
52+
53+
sth, err := tl.Backend.Fetch(context.Background(), "sth")
54+
fatalIfErr(t, err)
55+
v, err := tlogx.NewRFC6962Verifier("example.com/TestLog", tl.Key.Public())
56+
fatalIfErr(t, err)
57+
v.Timestamp = func(t uint64) { sthTimestamp = t }
58+
n, err := note.Open(sth, note.VerifierList(v))
59+
fatalIfErr(t, err)
60+
c, err := tlogx.ParseCheckpoint(n.Text)
61+
fatalIfErr(t, err)
62+
63+
if c.Origin != "example.com/TestLog" {
64+
t.Errorf("origin line is %q", c.Origin)
65+
}
66+
if c.Extension != "" {
67+
t.Errorf("unexpected extension %q", c.Extension)
68+
}
69+
70+
if c.N == 0 {
71+
if c.Hash != (tlog.Hash{}) {
72+
t.Error("empty log should have zero hash")
73+
}
74+
return
75+
}
76+
77+
tree := tlog.Tree{N: c.N, Hash: c.Hash}
78+
var indexes []int64
79+
for n := int64(0); n < c.N; n++ {
80+
indexes = append(indexes, tlog.StoredHashIndex(0, n))
81+
}
82+
// tlog.TileHashReader checks the inclusion of every hash in the provided
83+
// tree, so this checks the validity of the whole Merkle tree.
84+
leafHashes, err := tlog.TileHashReader(tree, (*tileReader)(tl)).ReadHashes(indexes)
85+
fatalIfErr(t, err)
86+
87+
lastTile := tlog.TileForIndex(tileHeight, tlog.StoredHashIndex(0, c.N-1))
88+
lastTile.L = -1
89+
for n := int64(0); n <= lastTile.N; n++ {
90+
tile := tlog.Tile{H: tileHeight, L: -1, N: n, W: tileWidth}
91+
if n == lastTile.N {
92+
tile = lastTile
93+
}
94+
b, err := tl.Backend.Fetch(context.Background(), tile.Path())
95+
fatalIfErr(t, err)
96+
s := cryptobyte.String(b)
97+
for i := 0; i < tile.W; i++ {
98+
start := len(b) - len(s)
99+
var timestamp uint64
100+
var entryType uint8
101+
var cert, extensions []byte
102+
if !s.ReadUint64(&timestamp) || !s.ReadUint8(&entryType) {
103+
t.Fatalf("invalid data tile %v around index %d", tile, len(b)-len(s))
104+
}
105+
switch entryType {
106+
case 0: // x509_entry
107+
if !s.ReadUint24LengthPrefixed((*cryptobyte.String)(&cert)) {
108+
t.Fatalf("invalid data tile %v around index %d", tile, len(b)-len(s))
109+
}
110+
case 1: // precert_entry
111+
panic("unimplemented") // TODO
112+
default:
113+
t.Fatalf("invalid data tile %v: unknown type %d", tile, entryType)
114+
}
115+
if !s.ReadUint16LengthPrefixed((*cryptobyte.String)(&extensions)) {
116+
t.Fatalf("invalid data tile %v around index %d", tile, len(b)-len(s))
117+
}
118+
end := len(b) - len(s)
119+
120+
idx := n*tileWidth + int64(i)
121+
if timestamp > sthTimestamp {
122+
t.Errorf("STH timestamp %d is before record %d timestamp %d", sthTimestamp, idx, timestamp)
123+
}
124+
got := tlog.RecordHash(append([]byte{0, 0}, b[start:end]...))
125+
if exp := leafHashes[idx]; got != exp {
126+
t.Errorf("tile leaf entry %d hashes to %v, level 0 hash is %v", idx, got, exp)
127+
}
128+
}
129+
if !s.Empty() {
130+
t.Errorf("invalid data tile %v: trailing data", tile)
131+
}
132+
}
133+
134+
return
135+
}
136+
137+
const tileHeight = 10
138+
const tileWidth = 1 << tileHeight
139+
140+
type tileReader TestLog
141+
142+
func (r *tileReader) Height() int {
143+
return tileHeight
144+
}
145+
146+
func (r *tileReader) ReadTiles(tiles []tlog.Tile) (data [][]byte, err error) {
147+
for _, t := range tiles {
148+
b, err := r.Backend.Fetch(context.Background(), t.Path())
149+
if err != nil {
150+
return nil, err
151+
}
152+
data = append(data, b)
153+
}
154+
return data, nil
155+
}
156+
157+
func (r *tileReader) SaveTiles(tiles []tlog.Tile, data [][]byte) {}
158+
159+
type verifier struct {
160+
name string
161+
hash uint32
162+
verify func(msg, sig []byte) bool
163+
}
164+
165+
func (v *verifier) Name() string { return v.name }
166+
func (v *verifier) KeyHash() uint32 { return v.hash }
167+
func (v *verifier) Verify(msg, sig []byte) bool { return v.verify(msg, sig) }
168+
37169
type MemoryBackend struct {
38170
t testing.TB
39171
mu sync.Mutex
@@ -47,6 +179,10 @@ func NewMemoryBackend(t testing.TB) *MemoryBackend {
47179
}
48180

49181
func (b *MemoryBackend) Upload(ctx context.Context, key string, data []byte) error {
182+
// TODO: check key format is expected.
183+
if len(data) == 0 {
184+
b.t.Errorf("uploaded key %q with empty data", key)
185+
}
50186
if err := ctx.Err(); err != nil {
51187
return err
52188
}
@@ -55,3 +191,23 @@ func (b *MemoryBackend) Upload(ctx context.Context, key string, data []byte) err
55191
b.m[key] = data
56192
return nil
57193
}
194+
195+
func (b *MemoryBackend) Fetch(ctx context.Context, key string) ([]byte, error) {
196+
if err := ctx.Err(); err != nil {
197+
return nil, err
198+
}
199+
b.mu.Lock()
200+
defer b.mu.Unlock()
201+
data, ok := b.m[key]
202+
if !ok {
203+
return nil, fmt.Errorf("key %q not found", key)
204+
}
205+
return data, nil
206+
}
207+
208+
func fatalIfErr(t testing.TB, err error) {
209+
t.Helper()
210+
if err != nil {
211+
t.Fatal(err)
212+
}
213+
}

0 commit comments

Comments
 (0)