diff --git a/README.md b/README.md index b6b58d26..94304814 100644 --- a/README.md +++ b/README.md @@ -19,20 +19,6 @@ At the moment, these storage backends are supported: - [AWS](./cmd/aws/): [deployment instructions](./deployment/live/aws/test/) - more to come soon! -## Working on the Code -The following files are auto-generated: - - [`mock_ct_storage.go`](./mockstorage/mock_ct_storage.go): a mock CT storage implementation for tests - -To re-generate these files, first install the right tools: - - [mockgen](https://github.com/golang/mock?tab=readme-ov-file#installation) - -Then, generate the files: - -```bash -cd $(go list -f '{{ .Dir }}' github.com/transparency-dev/static-ct); \ -go generate -x ./... # hunts for //go:generate comments and runs them -``` - ### Contact - Slack: https://transparency-dev.slack.com/ ([invitation](https://join.slack.com/t/transparency-dev/shared_invite/zt-27pkqo21d-okUFhur7YZ0rFoJVIOPznQ)) diff --git a/go.mod b/go.mod index df4d24ea..89c5647f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/transparency-dev/static-ct go 1.24.0 + require ( cloud.google.com/go/secretmanager v1.14.6 cloud.google.com/go/spanner v1.78.0 @@ -13,7 +14,6 @@ require ( github.com/aws/smithy-go v1.22.3 github.com/gdamore/tcell/v2 v2.8.1 github.com/go-sql-driver/mysql v1.9.1 - github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 github.com/kylelemons/godebug v1.1.0 github.com/prometheus/client_golang v1.21.1 @@ -27,7 +27,6 @@ require ( golang.org/x/net v0.38.0 google.golang.org/api v0.228.0 google.golang.org/grpc v1.71.0 - google.golang.org/protobuf v1.36.6 k8s.io/klog/v2 v2.130.1 ) @@ -105,4 +104,5 @@ require ( google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index ef7f98a2..1bb35b3b 100644 --- a/go.sum +++ b/go.sum @@ -1448,7 +1448,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= diff --git a/internal/scti/handlers_test.go b/internal/scti/handlers_test.go index f4782769..8632ebe3 100644 --- a/internal/scti/handlers_test.go +++ b/internal/scti/handlers_test.go @@ -18,56 +18,52 @@ import ( "bufio" "bytes" "context" - "crypto" - "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" "encoding/pem" - "fmt" "io" "net/http" "net/http/httptest" + "path" "strings" "testing" "time" - "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/transparency-dev/static-ct/internal/testdata" + "github.com/transparency-dev/static-ct/internal/testonly/storage/posix" "github.com/transparency-dev/static-ct/internal/types" "github.com/transparency-dev/static-ct/internal/x509util" - "github.com/transparency-dev/static-ct/mockstorage" - "github.com/transparency-dev/static-ct/modules/dedup" + "github.com/transparency-dev/static-ct/storage" + "github.com/transparency-dev/static-ct/storage/bbolt" tessera "github.com/transparency-dev/trillian-tessera" - "github.com/transparency-dev/trillian-tessera/ctonly" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" + posixTessera "github.com/transparency-dev/trillian-tessera/storage/posix" + "golang.org/x/mod/sumdb/note" "k8s.io/klog/v2" ) -// Arbitrary time for use in tests -var fakeTime = time.Date(2016, 7, 22, 11, 01, 13, 0, time.UTC) -var fakeTimeMillis = uint64(fakeTime.UnixNano() / nanosPerMilli) +var ( + // Test root + testRootPath = "../testdata/test_root_ca_cert.pem" -// Arbitrary origin for tests -var origin = "example.com" -var prefix = "/" + origin + // Arbitrary time for use in tests + fakeTime = time.Date(2016, 7, 22, 11, 01, 13, 0, time.UTC) + fakeTimeMillis = uint64(fakeTime.UnixNano() / nanosPerMilli) -// The deadline should be the above bumped by 500ms -var fakeDeadlineTime = time.Date(2016, 7, 22, 11, 01, 13, 500*1000*1000, time.UTC) -var fakeTimeSource = newFixedTimeSource(fakeTime) + // Arbitrary origin for tests + origin = "example.com" + prefix = "/" + origin -var entrypaths = []string{prefix + types.AddChainPath, prefix + types.AddPreChainPath, prefix + types.GetRootsPath} - -type handlerTestInfo struct { - mockCtrl *gomock.Controller - roots *x509util.PEMCertPool - storage *mockstorage.MockStorage - handlers map[string]appHandler -} + // Default handler options for tests + hOpts = HandlerOptions{ + Deadline: time.Millisecond * 500, + RequestLog: &DefaultRequestLog{}, + MaskInternalErrors: false, + TimeSource: newFixedTimeSource(fakeTime), + } +) type fixedTimeSource struct { fakeTime time.Time @@ -83,82 +79,129 @@ func (f *fixedTimeSource) Now() time.Time { return f.fakeTime } -// setupTest creates mock objects and contexts. Caller should invoke info.mockCtrl.Finish(). -func setupTest(t *testing.T, pemRoots []string, signer crypto.Signer) handlerTestInfo { +// setupTestLog creates a test TesseraCT log using a POSIX backend. +func setupTestLog(t *testing.T) *log { t.Helper() - info := handlerTestInfo{ - mockCtrl: gomock.NewController(t), - roots: x509util.NewPEMCertPool(), + + signer, err := setupSigner(fakeSignature) + if err != nil { + t.Fatalf("Failed to create test signer: %v", err) } - info.storage = mockstorage.NewMockStorage(info.mockCtrl) - vOpts := ChainValidationOpts{ - trustedRoots: info.roots, - rejectExpired: false, + roots := x509util.NewPEMCertPool() + if err := roots.AppendCertsFromPEMFile(testRootPath); err != nil { + t.Fatalf("Failed to read trusted roots: %v", err) } - hOpts := HandlerOptions{ - Deadline: time.Millisecond * 500, - RequestLog: new(DefaultRequestLog), - TimeSource: fakeTimeSource, + cvOpts := ChainValidationOpts{ + trustedRoots: roots, + rejectExpired: false, + rejectUnexpired: false, } - signSCT := func(leaf *types.MerkleTreeLeaf) (*types.SignedCertificateTimestamp, error) { - return buildV1SCT(signer, leaf) + + log, err := NewLog(t.Context(), origin, signer, cvOpts, newPosixStorageFunc(t), newFixedTimeSource(fakeTime)) + if err != nil { + t.Fatalf("newLog(): %v", err) } - log := log{ - storage: info.storage, - signSCT: signSCT, - origin: origin, - chainValidationOpts: vOpts, + + return log +} + +// setupTestServer creates a test TesseraCT server with a single endpoint at path. +func setupTestServer(t *testing.T, log *log, path string) *httptest.Server { + t.Helper() + + handlers := NewPathHandlers(&hOpts, log) + handler, ok := handlers[path] + if !ok { + t.Fatalf("Handler not found: %s", path) } - info.handlers = NewPathHandlers(&hOpts, &log) - for _, pemRoot := range pemRoots { - if !info.roots.AppendCertsFromPEM([]byte(pemRoot)) { - klog.Fatal("failed to load cert pool") + return httptest.NewServer(handler) +} + +// newPosixStorageFunc returns a function to create a new storage.CTStorage instance with: +// - a POSIX Tessera storage driver +// - a POSIX issuer storage system +// - a BBolt deduplication database +func newPosixStorageFunc(t *testing.T) storage.CreateStorage { + t.Helper() + return func(ctx context.Context, signer note.Signer) (*storage.CTStorage, error) { + driver, err := posixTessera.New(ctx, path.Join(t.TempDir(), "log")) + if err != nil { + klog.Fatalf("Failed to initialize POSIX Tessera storage driver: %v", err) } - } - return info + opts := tessera.NewAppendOptions(). + WithCheckpointSigner(signer). + WithCTLayout() + // TODO(phboneff): add other options like MaxBatchSize of 1 when implementing + // additional tests + + appender, _, _, err := tessera.NewAppender(ctx, driver, opts) + if err != nil { + klog.Fatalf("Failed to initialize POSIX Tessera appender: %v", err) + } + + issuerStorage, err := posix.NewIssuerStorage(t.TempDir()) + if err != nil { + klog.Fatalf("failed to initialize InMemory issuer storage: %v", err) + } + + beDedupStorage, err := bbolt.NewStorage(path.Join(t.TempDir(), "dedup.db")) + if err != nil { + klog.Fatalf("Failed to initialize BBolt deduplication database: %v", err) + } + + s, err := storage.NewCTStorage(appender, issuerStorage, beDedupStorage) + if err != nil { + klog.Fatalf("Failed to initialize CTStorage: %v", err) + } + return s, nil + } } -func (info handlerTestInfo) getHandlers(t *testing.T) pathHandlers { +func getHandlers(t *testing.T, handlers pathHandlers) pathHandlers { t.Helper() - handler, ok := info.handlers[prefix+types.GetRootsPath] + path := path.Join(prefix, types.GetRootsPath) + handler, ok := handlers[path] if !ok { t.Fatalf("%q path not registered", types.GetRootsPath) } - return pathHandlers{prefix + types.GetRootsPath: handler} + return pathHandlers{path: handler} } -func (info handlerTestInfo) postHandlers(t *testing.T) pathHandlers { +func postHandlers(t *testing.T, handlers pathHandlers) pathHandlers { t.Helper() - addChainHandler, ok := info.handlers[prefix+types.AddChainPath] + addChainPath := path.Join(prefix, types.AddChainPath) + addPreChainPath := path.Join(prefix, types.AddPreChainPath) + + addChainHandler, ok := handlers[addChainPath] if !ok { t.Fatalf("%q path not registered", types.AddPreChainStr) } - addPreChainHandler, ok := info.handlers[prefix+types.AddPreChainPath] + addPreChainHandler, ok := handlers[addPreChainPath] if !ok { t.Fatalf("%q path not registered", types.AddPreChainStr) } return map[string]appHandler{ - prefix + types.AddChainPath: addChainHandler, - prefix + types.AddPreChainPath: addPreChainHandler, + addChainPath: addChainHandler, + addPreChainPath: addPreChainHandler, } } func TestPostHandlersRejectGet(t *testing.T) { - info := setupTest(t, []string{testdata.CACertPEM}, nil) - defer info.mockCtrl.Finish() + log := setupTestLog(t) + handlers := NewPathHandlers(&hOpts, log) // Anything in the post handler list should reject GET - for path, handler := range info.postHandlers(t) { + for path, handler := range postHandlers(t, handlers) { t.Run(path, func(t *testing.T) { s := httptest.NewServer(handler) defer s.Close() - resp, err := http.Get(s.URL + "/ct/v1/" + path) + resp, err := http.Get(s.URL + path) if err != nil { t.Fatalf("http.Get(%s)=(_,%q); want (_,nil)", path, err) } @@ -170,16 +213,16 @@ func TestPostHandlersRejectGet(t *testing.T) { } func TestGetHandlersRejectPost(t *testing.T) { - info := setupTest(t, []string{testdata.CACertPEM}, nil) - defer info.mockCtrl.Finish() + log := setupTestLog(t) + handlers := NewPathHandlers(&hOpts, log) // Anything in the get handler list should reject POST. - for path, handler := range info.getHandlers(t) { + for path, handler := range getHandlers(t, handlers) { t.Run(path, func(t *testing.T) { s := httptest.NewServer(handler) defer s.Close() - resp, err := http.Post(s.URL+"/ct/v1/"+path, "application/json", nil) + resp, err := http.Post(s.URL+path, "application/json", nil) if err != nil { t.Fatalf("http.Post(%s)=(_,%q); want (_,nil)", path, err) } @@ -203,14 +246,15 @@ func TestPostHandlersFailure(t *testing.T) { {"wrong-chain", strings.NewReader(`{ "chain": [ "test" ] }`), http.StatusBadRequest}, } - info := setupTest(t, []string{testdata.CACertPEM}, nil) - defer info.mockCtrl.Finish() - for path, handler := range info.postHandlers(t) { + log := setupTestLog(t) + handlers := NewPathHandlers(&hOpts, log) + + for path, handler := range postHandlers(t, handlers) { t.Run(path, func(t *testing.T) { s := httptest.NewServer(handler) for _, test := range tests { - resp, err := http.Post(s.URL+"/ct/v1/"+path, "application/json", test.body) + resp, err := http.Post(s.URL+path, "application/json", test.body) if err != nil { t.Errorf("http.Post(%s,%s)=(_,%q); want (_,nil)", path, test.descr, err) continue @@ -223,11 +267,10 @@ func TestPostHandlersFailure(t *testing.T) { } } -func TestHandlers(t *testing.T) { - info := setupTest(t, nil, nil) - defer info.mockCtrl.Finish() +func TestNewPathHandlers(t *testing.T) { + log := setupTestLog(t) t.Run("Handlers", func(t *testing.T) { - handlers := info.handlers + handlers := NewPathHandlers(&HandlerOptions{}, log) // Check each entrypoint has a handler if got, want := len(handlers), len(entrypoints); got != want { t.Fatalf("len(info.handler)=%d; want %d", got, want) @@ -247,6 +290,7 @@ func TestHandlers(t *testing.T) { t.Errorf("Handler names mismatch got: %v, want: %v", hNames, entrypoints) } + entrypaths := []string{prefix + types.AddChainPath, prefix + types.AddPreChainPath, prefix + types.GetRootsPath} if !cmp.Equal(entrypaths, hPaths, cmpopts.SortSlices(func(n1, n2 string) bool { return n1 < n2 })) { @@ -255,35 +299,64 @@ func TestHandlers(t *testing.T) { }) } -func parseChain(t *testing.T, isPrecert bool, pemChain []string, root *x509.Certificate) (*ctonly.Entry, []*x509.Certificate) { - t.Helper() - pool := loadCertsIntoPoolOrDie(t, pemChain) - leafChain := pool.RawCertificates() - if !leafChain[len(leafChain)-1].Equal(root) { - // The submitted chain may not include a root, but the generated LogLeaf will - fullChain := make([]*x509.Certificate, len(leafChain)+1) - copy(fullChain, leafChain) - fullChain[len(leafChain)] = root - leafChain = fullChain - } - entry, err := entryFromChain(leafChain, isPrecert, fakeTimeMillis) +// TODO(phboneff): use this in followup PR. Leaving here for now to make +// diffs easier to digest in PRs. +// func parseChain(t *testing.T, isPrecert bool, pemChain []string, root *x509.Certificate) (*ctonly.Entry, []*x509.Certificate) { +// t.Helper() +// pool := loadCertsIntoPoolOrDie(t, pemChain) +// leafChain := pool.RawCertificates() +// if !leafChain[len(leafChain)-1].Equal(root) { +// // The submitted chain may not include a root, but the generated LogLeaf will +// fullChain := make([]*x509.Certificate, len(leafChain)+1) +// copy(fullChain, leafChain) +// fullChain[len(leafChain)] = root +// leafChain = fullChain +// } +// entry, err := entryFromChain(leafChain, isPrecert, fakeTimeMillis) +// if err != nil { +// t.Fatalf("failed to create entry") +// } +// return entry, leafChain +// } + +func TestGetRoots(t *testing.T) { + log := setupTestLog(t) + server := setupTestServer(t, log, path.Join(prefix, types.GetRootsPath)) + defer server.Close() + + resp, err := http.Get(server.URL + path.Join(prefix, types.GetRootsPath)) if err != nil { - t.Fatalf("failed to create entry") + t.Fatalf("Failed to get roots: %v", err) } - return entry, leafChain -} -func TestAddChainWhitespace(t *testing.T) { - signer, err := setupSigner(fakeSignature) + if resp.StatusCode != http.StatusOK { + t.Errorf("Unexpected status code: %v", resp.StatusCode) + } + + var roots types.GetRootsResponse + err = json.NewDecoder(resp.Body).Decode(&roots) if err != nil { - t.Fatalf("Failed to create test signer: %v", err) + t.Errorf("Failed to decode response: %v", err) } - info := setupTest(t, []string{testdata.CACertPEM}, signer) - defer info.mockCtrl.Finish() + if got, want := len(roots.Certificates), 1; got != want { + t.Errorf("Unexpected number of certificates: got %d, want %d", got, want) + } + got, err := base64.StdEncoding.DecodeString(roots.Certificates[0]) + if err != nil { + t.Errorf("Failed to decode certificate: %v", err) + } + want, _ := pem.Decode([]byte(testdata.CACertPEM)) + if !bytes.Equal(got, want.Bytes) { + t.Errorf("Unexpected root: got %s, want %s", roots.Certificates[0], base64.StdEncoding.EncodeToString(want.Bytes)) + } +} + +// TODO(phboneff): this could just be a parseBodyJSONChain test +func TestAddChainWhitespace(t *testing.T) { // Throughout we use variants of a hard-coded POST body derived from a chain of: - pemChain := []string{testdata.CertFromIntermediate, testdata.IntermediateFromRoot} + // testdata.LeafSignedByFakeIntermediateCertPEM, testdata.FakeIntermediateCertPEM cert, rest := pem.Decode([]byte(testdata.CertFromIntermediate)) if len(rest) > 0 { t.Fatalf("got %d bytes remaining after decoding cert, want 0", len(rest)) @@ -305,9 +378,6 @@ func TestAddChainWhitespace(t *testing.T) { chunk2 := "\"" + intermediateB64 + "\"" epilog := "]}\n" - req, leafChain := parseChain(t, false, pemChain, info.roots.RawCertificates()[0]) - rsp := tessera.Index{Index: 0} - var tests = []struct { descr string body string @@ -340,28 +410,18 @@ func TestAddChainWhitespace(t *testing.T) { }, } + log := setupTestLog(t) + server := setupTestServer(t, log, path.Join(prefix, types.AddChainPath)) + defer server.Close() + for _, test := range tests { t.Run(test.descr, func(t *testing.T) { - if test.want == http.StatusOK { - info.storage.EXPECT().GetCertDedupInfo(deadlineMatcher(), cmpMatcher{leafChain[0]}).Return(dedup.SCTDedupInfo{Idx: uint64(0), Timestamp: fakeTimeMillis}, false, nil) - info.storage.EXPECT().AddIssuerChain(deadlineMatcher(), cmpMatcher{leafChain[1:]}).Return(nil) - info.storage.EXPECT().Add(deadlineMatcher(), cmpMatcher{req}).Return(func() (tessera.Index, error) { return rsp, nil }) - info.storage.EXPECT().AddCertDedupInfo(deadlineMatcher(), cmpMatcher{leafChain[0]}, dedup.SCTDedupInfo{Idx: uint64(0), Timestamp: fakeTimeMillis}).Return(nil) - } - - recorder := httptest.NewRecorder() - handler, ok := info.handlers["/example.com/ct/v1/add-chain"] - if !ok { - t.Fatalf("%q path not registered", types.AddChainStr) - } - req, err := http.NewRequest(http.MethodPost, "http://example.com/ct/v1/add-chain", strings.NewReader(test.body)) + resp, err := http.Post(server.URL+types.AddChainPath, "application/json", strings.NewReader(test.body)) if err != nil { - t.Fatalf("Failed to create POST request: %v", err) + t.Fatalf("http.Post(%s)=(_,%q); want (_,nil)", types.AddChainPath, err) } - handler.ServeHTTP(recorder, req) - - if recorder.Code != test.want { - t.Fatalf("addChain()=%d (body:%v); want %dv", recorder.Code, recorder.Body, test.want) + if got, want := resp.StatusCode, test.want; got != want { + t.Errorf("http.Post(%s)=(%d,nil); want (%d,nil)", types.AddChainPath, got, want) } }) } @@ -371,10 +431,8 @@ func TestAddChain(t *testing.T) { var tests = []struct { descr string chain []string - // TODO(phboneff): can this be removed? - toSign string // hex-encoded - want int - err error + want int + err error }{ { descr: "leaf-only", @@ -387,87 +445,65 @@ func TestAddChain(t *testing.T) { want: http.StatusBadRequest, }, { - descr: "backend-storage-fail", - chain: []string{testdata.CertFromIntermediate, testdata.IntermediateFromRoot}, - toSign: "1337d72a403b6539f58896decba416d5d4b3603bfa03e1f94bb9b4e898af897d", - want: http.StatusInternalServerError, - err: status.Errorf(codes.Internal, "error"), - }, - { - descr: "success-without-root", - chain: []string{testdata.CertFromIntermediate, testdata.IntermediateFromRoot}, - toSign: "1337d72a403b6539f58896decba416d5d4b3603bfa03e1f94bb9b4e898af897d", - want: http.StatusOK, + descr: "success-without-root", + chain: []string{testdata.CertFromIntermediate, testdata.IntermediateFromRoot}, + want: http.StatusOK, }, { - descr: "success", - chain: []string{testdata.CertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM}, - toSign: "1337d72a403b6539f58896decba416d5d4b3603bfa03e1f94bb9b4e898af897d", - want: http.StatusOK, + descr: "success", + chain: []string{testdata.CertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM}, + want: http.StatusOK, }, } - signer, err := setupSigner(fakeSignature) - if err != nil { - t.Fatalf("Failed to create test signer: %v", err) - } - - info := setupTest(t, []string{testdata.CACertPEM}, signer) - defer info.mockCtrl.Finish() + log := setupTestLog(t) + server := setupTestServer(t, log, path.Join(prefix, types.AddChainPath)) + defer server.Close() for _, test := range tests { t.Run(test.descr, func(t *testing.T) { pool := loadCertsIntoPoolOrDie(t, test.chain) chain := createJSONChain(t, *pool) - if len(test.toSign) > 0 { - req, leafChain := parseChain(t, false, test.chain, info.roots.RawCertificates()[0]) - rsp := tessera.Index{Index: 0} - info.storage.EXPECT().GetCertDedupInfo(deadlineMatcher(), cmpMatcher{leafChain[0]}).Return(dedup.SCTDedupInfo{Idx: uint64(0), Timestamp: fakeTimeMillis}, false, nil) - info.storage.EXPECT().AddIssuerChain(deadlineMatcher(), cmpMatcher{leafChain[1:]}).Return(nil) - info.storage.EXPECT().Add(deadlineMatcher(), cmpMatcher{req}).Return(func() (tessera.Index, error) { return rsp, test.err }) - if test.want == http.StatusOK { - info.storage.EXPECT().AddCertDedupInfo(deadlineMatcher(), cmpMatcher{leafChain[0]}, dedup.SCTDedupInfo{Idx: uint64(0), Timestamp: fakeTimeMillis}).Return(nil) - } - } - recorder := makeAddChainRequest(t, info.handlers, chain) - if recorder.Code != test.want { - t.Fatalf("addChain()=%d (body:%v); want %dv", recorder.Code, recorder.Body, test.want) + resp, err := http.Post(server.URL+types.AddChainPath, "application/json", chain) + if err != nil { + t.Fatalf("http.Post(%s)=(_,%q); want (_,nil)", types.AddChainPath, err) + } + if got, want := resp.StatusCode, test.want; got != want { + t.Errorf("http.Post(%s)=(%d,nil); want (%d,nil)", types.AddChainPath, got, want) } if test.want == http.StatusOK { - var resp types.AddChainResponse - if err := json.NewDecoder(recorder.Body).Decode(&resp); err != nil { - t.Fatalf("json.Decode(%s)=%v; want nil", recorder.Body.Bytes(), err) + var gotRsp types.AddChainResponse + if err := json.NewDecoder(resp.Body).Decode(&gotRsp); err != nil { + t.Fatalf("json.Decode()=%v; want nil", err) } - - if got, want := types.Version(resp.SCTVersion), types.V1; got != want { + if got, want := types.Version(gotRsp.SCTVersion), types.V1; got != want { t.Errorf("resp.SCTVersion=%v; want %v", got, want) } - if got, want := resp.ID, demoLogID[:]; !bytes.Equal(got, want) { + if got, want := gotRsp.ID, demoLogID[:]; !bytes.Equal(got, want) { t.Errorf("resp.ID=%v; want %v", got, want) } - if got, want := resp.Timestamp, uint64(1469185273000); got != want { + if got, want := gotRsp.Timestamp, fakeTimeMillis; got != want { t.Errorf("resp.Timestamp=%d; want %d", got, want) } - if got, want := hex.EncodeToString(resp.Signature), "040300067369676e6564"; got != want { + if got, want := hex.EncodeToString(gotRsp.Signature), "040300067369676e6564"; got != want { t.Errorf("resp.Signature=%s; want %s", got, want) } + // TODO(phboneff): read from the log and compare values + // TODO(phboneff): add a test with a backend write failure // TODO(phboneff): check that the index is in the SCT - // TODO(phboneff): add a test with a not after range - // TODO(phboneff): add a test with a start date only + // TODO(phboneff): add duplicate tests } }) } } -func TestAddPrechain(t *testing.T) { +func TestAddPreChain(t *testing.T) { var tests = []struct { - descr string - chain []string - root string - toSign string // hex-encoded - err error - want int + descr string + chain []string + want int + err error }{ { descr: "leaf-signed-by-different", @@ -480,75 +516,59 @@ func TestAddPrechain(t *testing.T) { want: http.StatusBadRequest, }, { - descr: "backend-storage-fail", - chain: []string{testdata.PrecertPEMValid, testdata.CACertPEM}, - toSign: "92ecae1a2dc67a6c5f9c96fa5cab4c2faf27c48505b696dad926f161b0ca675a", - err: status.Errorf(codes.Internal, "error"), - want: http.StatusInternalServerError, + descr: "success", + chain: []string{testdata.PrecertPEMValid, testdata.CACertPEM}, + want: http.StatusOK, }, { - descr: "success", - chain: []string{testdata.PrecertPEMValid, testdata.CACertPEM}, - toSign: "92ecae1a2dc67a6c5f9c96fa5cab4c2faf27c48505b696dad926f161b0ca675a", - want: http.StatusOK, + descr: "success-with-intermediate", + chain: []string{testdata.PreCertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM}, + want: http.StatusOK, }, { - descr: "success-without-root", - chain: []string{testdata.PrecertPEMValid}, - toSign: "92ecae1a2dc67a6c5f9c96fa5cab4c2faf27c48505b696dad926f161b0ca675a", - want: http.StatusOK, + descr: "success-without-root", + chain: []string{testdata.PrecertPEMValid}, + want: http.StatusOK, }, - // TODO(phboneff): add a test with an intermediate - // TODO(phboneff): add a test with a pre-issuer intermediate cert - // TODO(phboneff): add a test with a not after range - // TODO(phboneff): add a test with a start date only - } - - signer, err := setupSigner(fakeSignature) - if err != nil { - t.Fatalf("Failed to create test signer: %v", err) } - info := setupTest(t, []string{testdata.CACertPEM}, signer) - defer info.mockCtrl.Finish() + log := setupTestLog(t) + server := setupTestServer(t, log, path.Join(prefix, types.AddPreChainPath)) + defer server.Close() for _, test := range tests { t.Run(test.descr, func(t *testing.T) { pool := loadCertsIntoPoolOrDie(t, test.chain) chain := createJSONChain(t, *pool) - if len(test.toSign) > 0 { - req, leafChain := parseChain(t, true, test.chain, info.roots.RawCertificates()[0]) - rsp := tessera.Index{Index: 0} - info.storage.EXPECT().GetCertDedupInfo(deadlineMatcher(), cmpMatcher{leafChain[0]}).Return(dedup.SCTDedupInfo{Idx: uint64(0), Timestamp: fakeTimeMillis}, false, nil) - info.storage.EXPECT().AddIssuerChain(deadlineMatcher(), cmpMatcher{leafChain[1:]}).Return(nil) - info.storage.EXPECT().Add(deadlineMatcher(), cmpMatcher{req}).Return(func() (tessera.Index, error) { return rsp, test.err }) - if test.want == http.StatusOK { - info.storage.EXPECT().AddCertDedupInfo(deadlineMatcher(), cmpMatcher{leafChain[0]}, dedup.SCTDedupInfo{Idx: uint64(0), Timestamp: fakeTimeMillis}).Return(nil) - } - } - recorder := makeAddPrechainRequest(t, info.handlers, chain) - if recorder.Code != test.want { - t.Fatalf("addPrechain()=%d (body:%v); want %d", recorder.Code, recorder.Body, test.want) + resp, err := http.Post(server.URL+types.AddPreChainPath, "application/json", chain) + if err != nil { + t.Fatalf("http.Post(%s)=(_,%q); want (_,nil)", types.AddPreChainPath, err) + } + if got, want := resp.StatusCode, test.want; got != want { + t.Errorf("http.Post(%s)=(%d,nil); want (%d,nil)", types.AddPreChainPath, got, want) } if test.want == http.StatusOK { - var resp types.AddChainResponse - if err := json.NewDecoder(recorder.Body).Decode(&resp); err != nil { - t.Fatalf("json.Decode(%s)=%v; want nil", recorder.Body.Bytes(), err) + var gotRsp types.AddChainResponse + if err := json.NewDecoder(resp.Body).Decode(&gotRsp); err != nil { + t.Fatalf("json.Decode()=%v; want nil", err) } - - if got, want := types.Version(resp.SCTVersion), types.V1; got != want { + if got, want := types.Version(gotRsp.SCTVersion), types.V1; got != want { t.Errorf("resp.SCTVersion=%v; want %v", got, want) } - if got, want := resp.ID, demoLogID[:]; !bytes.Equal(got, want) { - t.Errorf("resp.ID=%x; want %x", got, want) + if got, want := gotRsp.ID, demoLogID[:]; !bytes.Equal(got, want) { + t.Errorf("resp.ID=%v; want %v", got, want) } - if got, want := resp.Timestamp, fakeTimeMillis; got != want { + if got, want := gotRsp.Timestamp, fakeTimeMillis; got != want { t.Errorf("resp.Timestamp=%d; want %d", got, want) } - if got, want := hex.EncodeToString(resp.Signature), "040300067369676e6564"; got != want { + if got, want := hex.EncodeToString(gotRsp.Signature), "040300067369676e6564"; got != want { t.Errorf("resp.Signature=%s; want %s", got, want) } + // TODO(phboneff): read from the log and compare values + // TODO(phboneff): add a test with a backend write failure + // TODO(phboneff): check that the index is in the SCT + // TODO(phboneff): add duplicate tests } }) } @@ -576,62 +596,6 @@ func createJSONChain(t *testing.T, p x509util.PEMCertPool) io.Reader { return bufio.NewReader(&buffer) } -type dlMatcher struct { -} - -func deadlineMatcher() gomock.Matcher { - return dlMatcher{} -} - -func (d dlMatcher) Matches(x any) bool { - ctx, ok := x.(context.Context) - if !ok { - return false - } - - deadlineTime, ok := ctx.Deadline() - if !ok { - return false // we never make calls without a deadline set - } - - return deadlineTime.Equal(fakeDeadlineTime) -} - -func (d dlMatcher) String() string { - return fmt.Sprintf("deadline is %v", fakeDeadlineTime) -} - -func makeAddPrechainRequest(t *testing.T, handlers pathHandlers, body io.Reader) *httptest.ResponseRecorder { - t.Helper() - handler, ok := handlers[prefix+types.AddPreChainPath] - if !ok { - t.Fatalf("%q path not registered", types.AddPreChainStr) - } - return makeAddChainRequestInternal(t, handler, "add-pre-chain", body) -} - -func makeAddChainRequest(t *testing.T, handlers pathHandlers, body io.Reader) *httptest.ResponseRecorder { - t.Helper() - handler, ok := handlers[prefix+types.AddChainPath] - if !ok { - t.Fatalf("%q path not registered", types.AddChainStr) - } - return makeAddChainRequestInternal(t, handler, "add-chain", body) -} - -func makeAddChainRequestInternal(t *testing.T, handler appHandler, path string, body io.Reader) *httptest.ResponseRecorder { - t.Helper() - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://example.com/ct/v1/%s", path), body) - if err != nil { - t.Fatalf("Failed to create POST request: %v", err) - } - - w := httptest.NewRecorder() - handler.ServeHTTP(w, req) - - return w -} - func loadCertsIntoPoolOrDie(t *testing.T, certs []string) *x509util.PEMCertPool { t.Helper() pool := x509util.NewPEMCertPool() @@ -642,14 +606,3 @@ func loadCertsIntoPoolOrDie(t *testing.T, certs []string) *x509util.PEMCertPool } return pool } - -// cmpMatcher is a custom gomock.Matcher that uses cmp.Equal combined with a -// cmp.Comparer that knows how to properly compare proto.Message types. -type cmpMatcher struct{ want any } - -func (m cmpMatcher) Matches(got any) bool { - return cmp.Equal(got, m.want, cmp.Comparer(proto.Equal)) -} -func (m cmpMatcher) String() string { - return fmt.Sprintf("equals %v", m.want) -} diff --git a/internal/testonly/storage/posix/issuers.go b/internal/testonly/storage/posix/issuers.go new file mode 100644 index 00000000..6a5519ff --- /dev/null +++ b/internal/testonly/storage/posix/issuers.go @@ -0,0 +1,85 @@ +// Copyright 2025 The Tessera authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// package posix implements a test issuer storage system on a local filesystem. +// It is not fit for production use. +package posix + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path" + "strings" + + "github.com/transparency-dev/static-ct/storage" + "k8s.io/klog/v2" +) + +// IssuersStorage is a key value store backed by the local filesystem to store issuer chains. +type IssuersStorage string + +// NewIssuerStorage creates a new IssuerStorage. +// +// It creates the underying directory if it does not exist already. +func NewIssuerStorage(path string) (IssuersStorage, error) { + // Does nothing if the dictory already exists. + if err := os.MkdirAll(path, 0755); err != nil { + return "", fmt.Errorf("failed to create path %q: %v", path, err) + } + return IssuersStorage(path), nil +} + +// keyToObjName converts bytes to filesystem path. +// +// empty keys, and keys including a '/' character are not allowed to avoid +// confusion with directory names. This list of exclusions is not exhaustive, +// and does not guarantee that it will fit all filesystems. +func (s IssuersStorage) keyToObjName(key []byte) (string, error) { + if string(key) == "" { + return "", fmt.Errorf("key cannot be empty") + } + if strings.Contains(string(key), string(os.PathSeparator)) { + return "", fmt.Errorf("key %q cannot contain '/'", string(key)) + } + return path.Join(string(s), string(key)), nil +} + +// AddIssuers stores Issuers values under their Key if there isn't an object under Key already. +func (s IssuersStorage) AddIssuersIfNotExist(_ context.Context, kv []storage.KV) error { + for _, kv := range kv { + objName, err := s.keyToObjName(kv.K) + if err != nil { + return fmt.Errorf("failed to convert key to object name: %v", err) + } + // We first try and see if this issuer cert has already been stored. + if f, err := os.ReadFile(objName); err != nil { + if errors.Is(err, os.ErrNotExist) { + if err := os.WriteFile(objName, kv.V, 0644); err != nil { + return fmt.Errorf("failed to write object %q: %v", objName, err) + } + klog.V(2).Infof("AddIssuersIfNotExist: added %q", objName) + continue + } + return fmt.Errorf("failed to read object %q: %v", objName, err) + } else if bytes.Equal(f, kv.V) { + klog.V(2).Infof("AddIssuersIfNotExist: object %q already exists with identical contents, continuing", objName) + continue + } + return fmt.Errorf("object %q already exists with different content", objName) + } + return nil +} diff --git a/internal/testonly/storage/posix/issuers_test.go b/internal/testonly/storage/posix/issuers_test.go new file mode 100644 index 00000000..161aa819 --- /dev/null +++ b/internal/testonly/storage/posix/issuers_test.go @@ -0,0 +1,215 @@ +// Copyright 2025 The Tessera authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package posix + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/transparency-dev/static-ct/storage" +) + +func TestNewIssuerStorage(t *testing.T) { + tmpDir := t.TempDir() + + tests := []struct { + name string + path string + wantErr bool + }{ + { + name: "valid path", + path: tmpDir, + wantErr: false, + }, + { + name: "non-existent path", + path: filepath.Join(tmpDir, "nonexistent"), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewIssuerStorage(tt.path) + if (err != nil) != tt.wantErr { + t.Errorf("NewIssuerStorage() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func TestKeyToObjName(t *testing.T) { + tmpDir := t.TempDir() + s := IssuersStorage(tmpDir) + + tests := []struct { + name string + key []byte + want string + wantErr bool + }{ + { + name: "valid key", + key: []byte("issuer1"), + want: filepath.Join(tmpDir, "issuer1"), + wantErr: false, + }, + { + name: "empty key", + key: []byte(""), + wantErr: true, + }, + { + name: "key with os.PathSeparator", + key: []byte(fmt.Sprintf("issuer%s1", string(os.PathSeparator))), + want: "", + wantErr: true, + }, + { + name: "key with multiple slashes", + key: []byte(fmt.Sprintf("issuer%s1%s2", string(os.PathSeparator), string(os.PathSeparator))), + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := s.keyToObjName(tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("IssuersStorage.keyToObjName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("IssuersStorage.keyToObjName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAddIssuersIfNotExist(t *testing.T) { + tmpDir := t.TempDir() + s, err := NewIssuerStorage(tmpDir) + if err != nil { + t.Fatalf("NewIssuerStorage() failed: %v", err) + } + + tests := []struct { + name string + kv []storage.KV + want map[string][]byte + wantErr bool + }{ + { + name: "add single issuer", + kv: []storage.KV{ + {K: []byte("issuer1"), V: []byte("issuer1 data")}, + }, + want: map[string][]byte{ + "issuer1": []byte("issuer1 data"), + }, + wantErr: false, + }, + { + name: "add multiple issuers", + kv: []storage.KV{ + {K: []byte("issuer2"), V: []byte("issuer2 data")}, + {K: []byte("issuer3"), V: []byte("issuer3 data")}, + }, + want: map[string][]byte{ + "issuer2": []byte("issuer2 data"), + "issuer3": []byte("issuer3 data"), + }, + wantErr: false, + }, + { + name: "add existing issuer", + kv: []storage.KV{ + {K: []byte("issuer1"), V: []byte("issuer1 data")}, + }, + want: map[string][]byte{ + "issuer1": []byte("issuer1 data"), + }, + wantErr: false, + }, + { + name: "add existing issuer with different data", + kv: []storage.KV{ + {K: []byte("issuer1"), V: []byte("different data")}, + }, + want: map[string][]byte{ + "issuer1": []byte("issuer1 data"), + }, + wantErr: true, + }, + { + name: "add new issuer and existing issuer", + kv: []storage.KV{ + {K: []byte("issuer4"), V: []byte("issuer4 data")}, + {K: []byte("issuer1"), V: []byte("issuer1 data")}, + }, + want: map[string][]byte{ + "issuer1": []byte("issuer1 data"), + "issuer4": []byte("issuer4 data"), + }, + wantErr: false, + }, + { + name: "add issuer with invalid path", + kv: []storage.KV{ + {K: []byte("dir1/dir2/issuer5"), V: []byte("issuer5 data")}, + }, + want: map[string][]byte{}, + wantErr: true, + }, + { + name: "add issuer with empty path", + kv: []storage.KV{ + {K: []byte(""), V: []byte("issuer5 data")}, + }, + want: map[string][]byte{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := s.AddIssuersIfNotExist(context.Background(), tt.kv) + if (err != nil) != tt.wantErr { + t.Errorf("AddIssuersIfNotExist() error = %v, wantErr %v", err, tt.wantErr) + return + } + + for k, v := range tt.want { + objName, err := s.keyToObjName([]byte(k)) + if err != nil { + t.Errorf("Failed to convert key %q to object name: %v", k, err) + } + got, err := os.ReadFile(objName) + if err != nil { + t.Fatalf("Failed to read object %q: %v", objName, err) + } + if !reflect.DeepEqual(got, v) { + t.Errorf("Object %q content mismatch: got %v, want %v", objName, got, v) + } + } + }) + } +} diff --git a/internal/types/rfc6962.go b/internal/types/rfc6962.go index 2c50ec9a..a418093e 100644 --- a/internal/types/rfc6962.go +++ b/internal/types/rfc6962.go @@ -334,3 +334,8 @@ type AddChainResponse struct { Extensions string `json:"extensions"` // Holder for any CT extensions Signature []byte `json:"signature"` // Log signature for this SCT } + +// GetRootsResponse represents the JSON response to the get-roots GET method from section 4.7. +type GetRootsResponse struct { + Certificates []string `json:"certificates"` +} diff --git a/mockstorage/gen.go b/mockstorage/gen.go deleted file mode 100644 index 26382d4d..00000000 --- a/mockstorage/gen.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2016 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package mockclient provides a mockable version of the Trillian log client API. -package mockstorage - -//go:generate mockgen -package mockstorage -destination mock_ct_storage.go github.com/transparency-dev/static-ct/internal/scti Storage diff --git a/mockstorage/mock_ct_storage.go b/mockstorage/mock_ct_storage.go deleted file mode 100644 index 8582b4cb..00000000 --- a/mockstorage/mock_ct_storage.go +++ /dev/null @@ -1,97 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/transparency-dev/static-ct/internal/scti (interfaces: Storage) - -// Package mockstorage is a generated GoMock package. -package mockstorage - -import ( - context "context" - x509 "crypto/x509" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - dedup "github.com/transparency-dev/static-ct/modules/dedup" - tessera "github.com/transparency-dev/trillian-tessera" - ctonly "github.com/transparency-dev/trillian-tessera/ctonly" -) - -// MockStorage is a mock of Storage interface. -type MockStorage struct { - ctrl *gomock.Controller - recorder *MockStorageMockRecorder -} - -// MockStorageMockRecorder is the mock recorder for MockStorage. -type MockStorageMockRecorder struct { - mock *MockStorage -} - -// NewMockStorage creates a new mock instance. -func NewMockStorage(ctrl *gomock.Controller) *MockStorage { - mock := &MockStorage{ctrl: ctrl} - mock.recorder = &MockStorageMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStorage) EXPECT() *MockStorageMockRecorder { - return m.recorder -} - -// Add mocks base method. -func (m *MockStorage) Add(arg0 context.Context, arg1 *ctonly.Entry) tessera.IndexFuture { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Add", arg0, arg1) - ret0, _ := ret[0].(tessera.IndexFuture) - return ret0 -} - -// Add indicates an expected call of Add. -func (mr *MockStorageMockRecorder) Add(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockStorage)(nil).Add), arg0, arg1) -} - -// AddCertDedupInfo mocks base method. -func (m *MockStorage) AddCertDedupInfo(arg0 context.Context, arg1 *x509.Certificate, arg2 dedup.SCTDedupInfo) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddCertDedupInfo", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// AddCertDedupInfo indicates an expected call of AddCertDedupInfo. -func (mr *MockStorageMockRecorder) AddCertDedupInfo(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCertDedupInfo", reflect.TypeOf((*MockStorage)(nil).AddCertDedupInfo), arg0, arg1, arg2) -} - -// AddIssuerChain mocks base method. -func (m *MockStorage) AddIssuerChain(arg0 context.Context, arg1 []*x509.Certificate) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddIssuerChain", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// AddIssuerChain indicates an expected call of AddIssuerChain. -func (mr *MockStorageMockRecorder) AddIssuerChain(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIssuerChain", reflect.TypeOf((*MockStorage)(nil).AddIssuerChain), arg0, arg1) -} - -// GetCertDedupInfo mocks base method. -func (m *MockStorage) GetCertDedupInfo(arg0 context.Context, arg1 *x509.Certificate) (dedup.SCTDedupInfo, bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCertDedupInfo", arg0, arg1) - ret0, _ := ret[0].(dedup.SCTDedupInfo) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetCertDedupInfo indicates an expected call of GetCertDedupInfo. -func (mr *MockStorageMockRecorder) GetCertDedupInfo(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertDedupInfo", reflect.TypeOf((*MockStorage)(nil).GetCertDedupInfo), arg0, arg1) -}