package ctpolicy import ( "context" "errors" "regexp" "testing" "time" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/ctpolicy/ctconfig" berrors "github.com/letsencrypt/boulder/errors" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" pubpb "github.com/letsencrypt/boulder/publisher/proto" "github.com/letsencrypt/boulder/test" "github.com/prometheus/client_golang/prometheus" ) type mockPub struct { } func (mp *mockPub) SubmitToSingleCTWithResult(_ context.Context, _ *pubpb.Request) (*pubpb.Result, error) { return &pubpb.Result{Sct: []byte{0}}, nil } type alwaysFail struct { mockPub } func (mp *alwaysFail) SubmitToSingleCTWithResult(_ context.Context, _ *pubpb.Request) (*pubpb.Result, error) { return nil, errors.New("BAD") } func TestGetSCTs(t *testing.T) { expired, cancel := context.WithDeadline(context.Background(), time.Now()) defer cancel() missingSCTErr := berrors.MissingSCTs testCases := []struct { name string mock core.Publisher groups []ctconfig.CTGroup ctx context.Context result core.SCTDERs errRegexp *regexp.Regexp berrorType *berrors.ErrorType }{ { name: "basic success case", mock: &mockPub{}, groups: []ctconfig.CTGroup{ { Name: "a", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, {URI: "ghi", Key: "jkl"}, }, }, { Name: "b", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, {URI: "ghi", Key: "jkl"}, }, }, }, ctx: context.Background(), result: core.SCTDERs{[]byte{0}, []byte{0}}, }, { name: "basic failure case", mock: &alwaysFail{}, groups: []ctconfig.CTGroup{ { Name: "a", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, {URI: "ghi", Key: "jkl"}, }, }, { Name: "b", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, {URI: "ghi", Key: "jkl"}, }, }, }, ctx: context.Background(), errRegexp: regexp.MustCompile("CT log group \".\": all submissions failed"), berrorType: &missingSCTErr, }, { name: "parent context timeout failure case", mock: &alwaysFail{}, groups: []ctconfig.CTGroup{ { Name: "a", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, {URI: "ghi", Key: "jkl"}, }, }, { Name: "b", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, {URI: "ghi", Key: "jkl"}, }, }, }, ctx: expired, errRegexp: regexp.MustCompile("CT log group \".\": context deadline exceeded"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctp := New(tc.mock, tc.groups, nil, blog.NewMock(), metrics.NoopRegisterer) ret, err := ctp.GetSCTs(tc.ctx, []byte{0}, time.Time{}) if tc.result != nil { test.AssertDeepEquals(t, ret, tc.result) } else if tc.errRegexp != nil { if !tc.errRegexp.MatchString(err.Error()) { t.Errorf("Error %q did not match expected regexp %q", err, tc.errRegexp) } if tc.berrorType != nil { test.AssertErrorIs(t, err, *tc.berrorType) } } }) } } type failOne struct { mockPub badURL string } func (mp *failOne) SubmitToSingleCTWithResult(_ context.Context, req *pubpb.Request) (*pubpb.Result, error) { if req.LogURL == mp.badURL { return nil, errors.New("BAD") } return &pubpb.Result{Sct: []byte{0}}, nil } type slowPublisher struct { mockPub } func (sp *slowPublisher) SubmitToSingleCTWithResult(_ context.Context, req *pubpb.Request) (*pubpb.Result, error) { time.Sleep(time.Second) return &pubpb.Result{Sct: []byte{0}}, nil } func TestGetSCTsMetrics(t *testing.T) { ctp := New(&failOne{badURL: "abc"}, []ctconfig.CTGroup{ { Name: "a", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, {URI: "ghi", Key: "jkl"}, }, }, { Name: "b", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, {URI: "ghi", Key: "jkl"}, }, }, }, nil, blog.NewMock(), metrics.NoopRegisterer) _, err := ctp.GetSCTs(context.Background(), []byte{0}, time.Time{}) test.AssertNotError(t, err, "GetSCTs failed") test.AssertEquals(t, test.CountCounter(ctp.winnerCounter.With(prometheus.Labels{"log": "ghi", "group": "a"})), 1) test.AssertEquals(t, test.CountCounter(ctp.winnerCounter.With(prometheus.Labels{"log": "ghi", "group": "b"})), 1) } func TestGetSCTsFailMetrics(t *testing.T) { // When an entire log group fails, we should increment the "winner of SCT // race" stat for that group under the fictional log "all_failed". ctp := New(&failOne{badURL: "abc"}, []ctconfig.CTGroup{ { Name: "a", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, }, }, }, nil, blog.NewMock(), metrics.NoopRegisterer) _, err := ctp.GetSCTs(context.Background(), []byte{0}, time.Time{}) if err == nil { t.Fatal("GetSCTs should have failed") } test.AssertEquals(t, test.CountCounter(ctp.winnerCounter.With(prometheus.Labels{"log": "all_failed", "group": "a"})), 1) // Same thing, but for when an entire log group times out. ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() ctp = New(&slowPublisher{}, []ctconfig.CTGroup{ { Name: "a", Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, }, }, }, nil, blog.NewMock(), metrics.NoopRegisterer) _, err = ctp.GetSCTs(ctx, []byte{0}, time.Time{}) if err == nil { t.Fatal("GetSCTs should have failed") } test.AssertEquals(t, test.CountCounter(ctp.winnerCounter.With(prometheus.Labels{"log": "timeout", "group": "a"})), 1) } // A mock publisher that counts submissions type countEm struct { count int } func (ce *countEm) SubmitToSingleCTWithResult(_ context.Context, _ *pubpb.Request) (*pubpb.Result, error) { ce.count++ return &pubpb.Result{Sct: []byte{0}}, nil } func TestStagger(t *testing.T) { countingPub := &countEm{} ctp := New(countingPub, []ctconfig.CTGroup{ { Name: "a", Stagger: cmd.ConfigDuration{Duration: 500 * time.Millisecond}, Logs: []ctconfig.LogDescription{ {URI: "abc", Key: "def"}, {URI: "ghi", Key: "jkl"}, }, }, }, nil, blog.NewMock(), metrics.NoopRegisterer) _, err := ctp.GetSCTs(context.Background(), []byte{0}, time.Time{}) test.AssertNotError(t, err, "GetSCTs failed") if countingPub.count != 1 { t.Errorf("wrong number of requests to publisher. got %d, expected 1", countingPub.count) } }