1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package witness
16
17 import (
18 "context"
19 "crypto"
20 "crypto/ecdsa"
21 "crypto/elliptic"
22 "crypto/rand"
23 "errors"
24 "fmt"
25 "testing"
26 "time"
27
28 "github.com/go-redis/redismock/v9"
29 "github.com/golang/mock/gomock"
30 "github.com/google/trillian"
31 "github.com/google/trillian/types"
32 "github.com/prometheus/client_golang/prometheus"
33 "github.com/prometheus/client_golang/prometheus/testutil"
34 "github.com/sigstore/rekor/pkg/witness/mockclient"
35 "github.com/sigstore/sigstore/pkg/signature"
36 "go.uber.org/goleak"
37 )
38
39 func TestPublishCheckpoint(t *testing.T) {
40 treeID := 1234
41 hostname := "rekor-test"
42 freq := 1
43 counter := prometheus.NewCounterVec(prometheus.CounterOpts{
44 Name: "rekor_checkpoint_publish",
45 Help: "Checkpoint publishing by shard and code",
46 }, []string{"shard", "code"})
47 priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
48 signer, _ := signature.LoadSigner(priv, crypto.SHA256)
49
50 ctrl := gomock.NewController(t)
51 defer ctrl.Finish()
52
53 root := &types.LogRootV1{TreeSize: 10, RootHash: []byte{1}, TimestampNanos: 123, Revision: 0}
54 mRoot, err := root.MarshalBinary()
55 if err != nil {
56 t.Fatalf("error marshalling log root: %v", err)
57 }
58
59 mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
60 mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), &trillian.GetLatestSignedLogRootRequest{
61 LogId: int64(treeID),
62 FirstTreeSize: 0,
63 }).Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: mRoot}}, nil)
64
65 redisClient, mock := redismock.NewClientMock()
66 ts := time.Now().Truncate(time.Duration(freq) * time.Minute).UnixNano()
67 mock.Regexp().ExpectSetNX(fmt.Sprintf("%d/%d", treeID, ts), true, 0).SetVal(true)
68 mock.Regexp().ExpectSet(fmt.Sprintf("%d/latest", treeID), "[0-9a-fA-F]+", 0).SetVal("OK")
69
70 publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
71
72 ctx, cancel := context.WithCancel(context.Background())
73 publisher.StartPublisher(ctx)
74 defer cancel()
75
76
77 time.Sleep(1 * time.Second)
78
79 if err := mock.ExpectationsWereMet(); err != nil {
80 t.Error(err)
81 }
82
83 if res := testutil.CollectAndCount(counter); res != 2 {
84 t.Fatalf("unexpected number of metrics: %d", res)
85 }
86 if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(Success))); res != 1.0 {
87 t.Fatalf("unexpected number of metrics: %2f", res)
88 }
89 if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(SuccessObtainLock))); res != 1.0 {
90 t.Fatalf("unexpected number of metrics: %2f", res)
91 }
92 }
93
94 func TestPublishCheckpointMultiple(t *testing.T) {
95 treeID := 1234
96 hostname := "rekor-test"
97 freq := 1
98 counter := prometheus.NewCounterVec(prometheus.CounterOpts{
99 Name: "rekor_checkpoint_publish",
100 Help: "Checkpoint publishing by shard and code",
101 }, []string{"shard", "code"})
102 priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
103 signer, _ := signature.LoadSigner(priv, crypto.SHA256)
104
105 ctrl := gomock.NewController(t)
106 defer ctrl.Finish()
107
108 root := &types.LogRootV1{TreeSize: 10, RootHash: []byte{1}, TimestampNanos: 123, Revision: 0}
109 mRoot, err := root.MarshalBinary()
110 if err != nil {
111 t.Fatalf("error marshalling log root: %v", err)
112 }
113
114 mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
115 mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), &trillian.GetLatestSignedLogRootRequest{
116 LogId: int64(treeID),
117 FirstTreeSize: 0,
118 }).Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: mRoot}}, nil).MaxTimes(2)
119
120 redisClient, mock := redismock.NewClientMock()
121 ts := time.Now().Truncate(time.Duration(freq) * time.Minute).UnixNano()
122 mock.Regexp().ExpectSetNX(fmt.Sprintf("%d/%d", treeID, ts), true, 0).SetVal(true)
123 mock.Regexp().ExpectSet(fmt.Sprintf("%d/latest", treeID), "[0-9a-fA-F]+", 0).SetVal("OK")
124
125 publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
126 ctx, cancel := context.WithCancel(context.Background())
127 publisher.StartPublisher(ctx)
128 defer cancel()
129
130 redisClientEx, mockEx := redismock.NewClientMock()
131 mockEx.Regexp().ExpectSetNX(fmt.Sprintf("%d/%d", treeID, ts), true, 0).SetVal(false)
132 publisherEx := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClientEx, uint(freq), counter)
133 ctxEx, cancelEx := context.WithCancel(context.Background())
134 publisherEx.StartPublisher(ctxEx)
135 defer cancelEx()
136
137
138 time.Sleep(1 * time.Second)
139
140 if err := mock.ExpectationsWereMet(); err != nil {
141 t.Error(err)
142 }
143 if err := mockEx.ExpectationsWereMet(); err != nil {
144 t.Error(err)
145 }
146
147
148 if res := testutil.CollectAndCount(counter); res != 2 {
149 t.Fatalf("unexpected number of metrics: %d", res)
150 }
151 if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(Success))); res != 1.0 {
152 t.Fatalf("unexpected number of metrics: %2f", res)
153 }
154 if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(SuccessObtainLock))); res != 1.0 {
155 t.Fatalf("unexpected number of metrics: %2f", res)
156 }
157 }
158
159 func TestPublishCheckpointTrillianError(t *testing.T) {
160 treeID := 1234
161 hostname := "rekor-test"
162 freq := 1
163 counter := prometheus.NewCounterVec(prometheus.CounterOpts{
164 Name: "rekor_checkpoint_publish",
165 Help: "Checkpoint publishing by shard and code",
166 }, []string{"shard", "code"})
167 priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
168 signer, _ := signature.LoadSigner(priv, crypto.SHA256)
169
170 ctrl := gomock.NewController(t)
171 defer ctrl.Finish()
172
173
174 mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
175 mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).Return(nil, errors.New("error: LatestSLR"))
176
177 redisClient, _ := redismock.NewClientMock()
178
179 publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
180 ctx, cancel := context.WithCancel(context.Background())
181 publisher.StartPublisher(ctx)
182 defer cancel()
183
184
185 time.Sleep(1 * time.Second)
186
187 if res := testutil.CollectAndCount(counter); res != 1 {
188 t.Fatalf("unexpected number of metrics: %d", res)
189 }
190 if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(GetCheckpoint))); res != 1.0 {
191 t.Fatalf("unexpected number of metrics: %2f", res)
192 }
193 }
194
195 func TestPublishCheckpointInvalidTrillianResponse(t *testing.T) {
196 treeID := 1234
197 hostname := "rekor-test"
198 freq := 1
199 counter := prometheus.NewCounterVec(prometheus.CounterOpts{
200 Name: "rekor_checkpoint_publish",
201 Help: "Checkpoint publishing by shard and code",
202 }, []string{"shard", "code"})
203 priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
204 signer, _ := signature.LoadSigner(priv, crypto.SHA256)
205
206 ctrl := gomock.NewController(t)
207 defer ctrl.Finish()
208
209
210 mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
211 mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).
212 Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: []byte{}}}, nil)
213
214 redisClient, _ := redismock.NewClientMock()
215
216 publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
217 ctx, cancel := context.WithCancel(context.Background())
218 publisher.StartPublisher(ctx)
219 defer cancel()
220
221
222 time.Sleep(1 * time.Second)
223
224 if res := testutil.CollectAndCount(counter); res != 1 {
225 t.Fatalf("unexpected number of metrics: %d", res)
226 }
227 if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(UnmarshalCheckpoint))); res != 1.0 {
228 t.Fatalf("unexpected number of metrics: %2f", res)
229 }
230 }
231
232 func TestPublishCheckpointRedisFailure(t *testing.T) {
233 treeID := 1234
234 hostname := "rekor-test"
235 freq := 1
236 counter := prometheus.NewCounterVec(prometheus.CounterOpts{
237 Name: "rekor_checkpoint_publish",
238 Help: "Checkpoint publishing by shard and code",
239 }, []string{"shard", "code"})
240 priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
241 signer, _ := signature.LoadSigner(priv, crypto.SHA256)
242
243 ctrl := gomock.NewController(t)
244 defer ctrl.Finish()
245
246 root := &types.LogRootV1{TreeSize: 10, RootHash: []byte{1}, TimestampNanos: 123, Revision: 0}
247 mRoot, err := root.MarshalBinary()
248 if err != nil {
249 t.Fatalf("error marshalling log root: %v", err)
250 }
251
252 mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
253 mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).
254 Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: mRoot}}, nil)
255
256 redisClient, mock := redismock.NewClientMock()
257
258 mock.Regexp().ExpectSetNX(".+", true, 0).SetErr(errors.New("redis error"))
259
260 publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
261 ctx, cancel := context.WithCancel(context.Background())
262 publisher.StartPublisher(ctx)
263 defer cancel()
264
265
266 time.Sleep(1 * time.Second)
267
268 if res := testutil.CollectAndCount(counter); res != 1 {
269 t.Fatalf("unexpected number of metrics: %d", res)
270 }
271 if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(RedisFailure))); res != 1.0 {
272 t.Fatalf("unexpected number of metrics: %2f", res)
273 }
274 }
275
276 func TestPublishCheckpointRedisLatestFailure(t *testing.T) {
277 treeID := 1234
278 hostname := "rekor-test"
279 freq := 1
280 counter := prometheus.NewCounterVec(prometheus.CounterOpts{
281 Name: "rekor_checkpoint_publish",
282 Help: "Checkpoint publishing by shard and code",
283 }, []string{"shard", "code"})
284 priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
285 signer, _ := signature.LoadSigner(priv, crypto.SHA256)
286
287 ctrl := gomock.NewController(t)
288 defer ctrl.Finish()
289
290 root := &types.LogRootV1{TreeSize: 10, RootHash: []byte{1}, TimestampNanos: 123, Revision: 0}
291 mRoot, err := root.MarshalBinary()
292 if err != nil {
293 t.Fatalf("error marshalling log root: %v", err)
294 }
295
296 mockTrillianLogClient := mockclient.NewMockTrillianLogClient(ctrl)
297 mockTrillianLogClient.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).
298 Return(&trillian.GetLatestSignedLogRootResponse{SignedLogRoot: &trillian.SignedLogRoot{LogRoot: mRoot}}, nil)
299
300 redisClient, mock := redismock.NewClientMock()
301 mock.Regexp().ExpectSetNX(".+", true, 0).SetVal(true)
302
303 mock.Regexp().ExpectSet(".*", "[0-9a-fA-F]+", 0).SetErr(errors.New("error"))
304
305 publisher := NewCheckpointPublisher(context.Background(), mockTrillianLogClient, int64(treeID), hostname, signer, redisClient, uint(freq), counter)
306 ctx, cancel := context.WithCancel(context.Background())
307 publisher.StartPublisher(ctx)
308 defer cancel()
309
310
311 time.Sleep(1 * time.Second)
312
313
314 if res := testutil.CollectAndCount(counter); res != 2 {
315 t.Fatalf("unexpected number of metrics: %d", res)
316 }
317 if res := testutil.ToFloat64(counter.WithLabelValues(fmt.Sprint(treeID), fmt.Sprint(RedisLatestFailure))); res != 1.0 {
318 t.Fatalf("unexpected number of metrics: %2f", res)
319 }
320 }
321
322 func TestMain(m *testing.M) {
323 goleak.VerifyTestMain(m)
324 }
325
View as plain text