...

Source file src/github.com/sigstore/rekor/pkg/witness/publish_checkpoint_test.go

Documentation: github.com/sigstore/rekor/pkg/witness

     1  // Copyright 2023 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  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  	// wait for initial publish
    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  	// wait for initial publish
   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  	// only publishes once
   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  	// return error
   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  	// wait for initial publish
   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  	// set no log root in response
   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  	// wait for initial publish
   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  	// error on first redis call
   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  	// wait for initial publish
   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  	// error on second redis call
   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  	// wait for initial publish
   311  	time.Sleep(1 * time.Second)
   312  
   313  	// two metrics, one success for initial redis and one failure for latest
   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