diff --git a/pkg/api/api.go b/pkg/api/api.go index 5b267e3c6..0b41c0c6a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -25,10 +25,12 @@ import ( "strings" "github.com/google/trillian" + "github.com/google/trillian/types" "github.com/redis/go-redis/v9" "github.com/spf13/viper" "golang.org/x/exp/slices" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" @@ -39,6 +41,7 @@ import ( "github.com/sigstore/rekor/pkg/signer" "github.com/sigstore/rekor/pkg/storage" "github.com/sigstore/rekor/pkg/trillianclient" + "github.com/sigstore/rekor/pkg/util" "github.com/sigstore/rekor/pkg/witness" _ "github.com/sigstore/rekor/pkg/pubsub/gcp" // Load GCP pubsub implementation @@ -95,6 +98,11 @@ type API struct { // Publishes notifications when new entries are added to the log. May be // nil if no publisher is configured. newEntryPublisher pubsub.Publisher + // Stores map of inactive tree IDs to checkpoints + // Inactive shards will always return the same checkpoint, + // so we can fetch the checkpoint on service startup to + // minimize signature generations + cachedCheckpoints map[int64]string } func NewAPI(treeID uint) (*API, error) { @@ -132,6 +140,26 @@ func NewAPI(treeID uint) (*API, error) { return nil, fmt.Errorf("unable get sharding details from sharding config: %w", err) } + cachedCheckpoints := make(map[int64]string) + for _, r := range ranges.GetInactive() { + tc := trillianclient.NewTrillianClient(ctx, logClient, r.TreeID) + resp := tc.GetLatest(0) + if resp.Status != codes.OK { + return nil, fmt.Errorf("error with GetLatest(): resp code is %d", resp.Status) + } + result := resp.GetLatestResult + root := &types.LogRootV1{} + if err := root.UnmarshalBinary(result.SignedLogRoot.LogRoot); err != nil { + return nil, fmt.Errorf("error unmarshalling root: %w", err) + } + + cp, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), r.TreeID, uint64(r.TreeLength), root.RootHash, r.Signer) + if err != nil { + return nil, fmt.Errorf("error signing checkpoint for inactive shard %d: %w", r.TreeID, err) + } + cachedCheckpoints[r.TreeID] = string(cp) + } + var newEntryPublisher pubsub.Publisher if p := viper.GetString("rekor_server.new_entry_publisher"); p != "" { if !viper.GetBool("rekor_server.publish_events_protobuf") && !viper.GetBool("rekor_server.publish_events_json") { @@ -151,6 +179,7 @@ func NewAPI(treeID uint) (*API, error) { logRanges: ranges, // Utility functionality not required for operation of the core service newEntryPublisher: newEntryPublisher, + cachedCheckpoints: cachedCheckpoints, }, nil } diff --git a/pkg/api/entries.go b/pkg/api/entries.go index f96bac0e9..29bfec054 100644 --- a/pkg/api/entries.go +++ b/pkg/api/entries.go @@ -74,8 +74,8 @@ func signEntry(ctx context.Context, signer signature.Signer, entry models.LogEnt } // logEntryFromLeaf creates a signed LogEntry struct from trillian structs -func logEntryFromLeaf(ctx context.Context, _ trillianclient.TrillianClient, leaf *trillian.LogLeaf, - signedLogRoot *trillian.SignedLogRoot, proof *trillian.Proof, tid int64, ranges sharding.LogRanges) (models.LogEntry, error) { +func logEntryFromLeaf(ctx context.Context, leaf *trillian.LogLeaf, signedLogRoot *trillian.SignedLogRoot, + proof *trillian.Proof, tid int64, ranges sharding.LogRanges, cachedCheckpoints map[int64]string) (models.LogEntry, error) { log.ContextLogger(ctx).Debugf("log entry from leaf %d", leaf.GetLeafIndex()) root := &ttypes.LogRootV1{} @@ -105,9 +105,17 @@ func logEntryFromLeaf(ctx context.Context, _ trillianclient.TrillianClient, leaf return nil, fmt.Errorf("signing entry error: %w", err) } - scBytes, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), tid, root.TreeSize, root.RootHash, logRange.Signer) - if err != nil { - return nil, err + // If tree ID is inactive, use cached checkpoint + var sc string + val, ok := cachedCheckpoints[tid] + if ok { + sc = val + } else { + scBytes, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), tid, root.TreeSize, root.RootHash, logRange.Signer) + if err != nil { + return nil, err + } + sc = string(scBytes) } inclusionProof := models.InclusionProof{ @@ -115,7 +123,7 @@ func logEntryFromLeaf(ctx context.Context, _ trillianclient.TrillianClient, leaf RootHash: swag.String(hex.EncodeToString(root.RootHash)), LogIndex: swag.Int64(proof.GetLeafIndex()), Hashes: hashes, - Checkpoint: stringPointer(string(scBytes)), + Checkpoint: stringPointer(sc), } uuid := hex.EncodeToString(leaf.MerkleLeafHash) @@ -515,8 +523,7 @@ func SearchLogQueryHandler(params entries.SearchLogQueryParams) middleware.Respo if leafResp == nil { continue } - tcs := trillianclient.NewTrillianClient(httpReqCtx, api.logClient, shard) - logEntry, err := logEntryFromLeaf(httpReqCtx, tcs, leafResp.Leaf, leafResp.SignedLogRoot, leafResp.Proof, shard, api.logRanges) + logEntry, err := logEntryFromLeaf(httpReqCtx, leafResp.Leaf, leafResp.SignedLogRoot, leafResp.Proof, shard, api.logRanges, api.cachedCheckpoints) if err != nil { return handleRekorAPIError(params, http.StatusInternalServerError, err, err.Error()) } @@ -563,7 +570,7 @@ func retrieveLogEntryByIndex(ctx context.Context, logIndex int) (models.LogEntry return models.LogEntry{}, ErrNotFound } - return logEntryFromLeaf(ctx, tc, leaf, result.SignedLogRoot, result.Proof, tid, api.logRanges) + return logEntryFromLeaf(ctx, leaf, result.SignedLogRoot, result.Proof, tid, api.logRanges, api.cachedCheckpoints) } // Retrieve a Log Entry @@ -628,7 +635,7 @@ func retrieveUUIDFromTree(ctx context.Context, uuid string, tid int64) (models.L return models.LogEntry{}, err } - logEntry, err := logEntryFromLeaf(ctx, tc, result.Leaf, result.SignedLogRoot, result.Proof, tid, api.logRanges) + logEntry, err := logEntryFromLeaf(ctx, result.Leaf, result.SignedLogRoot, result.Proof, tid, api.logRanges, api.cachedCheckpoints) if err != nil { return models.LogEntry{}, fmt.Errorf("could not create log entry from leaf: %w", err) } diff --git a/pkg/api/tlog.go b/pkg/api/tlog.go index 83bb4d434..aeb1d7810 100644 --- a/pkg/api/tlog.go +++ b/pkg/api/tlog.go @@ -33,7 +33,6 @@ import ( "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/trillianclient" "github.com/sigstore/rekor/pkg/util" - "github.com/sigstore/sigstore/pkg/signature" ) // GetLogInfoHandler returns the current size of the tree and the STH @@ -44,7 +43,7 @@ func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder { var inactiveShards []*models.InactiveShardLogInfo for _, shard := range api.logRanges.GetInactive() { // Get details for this inactive shard - is, err := inactiveShardLogInfo(params.HTTPRequest.Context(), shard.TreeID, shard.Signer) + is, err := inactiveShardLogInfo(params.HTTPRequest.Context(), shard.TreeID, api.cachedCheckpoints) if err != nil { return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("inactive shard error: %w", err), unexpectedInactiveShardError) } @@ -168,7 +167,7 @@ func GetLogProofHandler(params tlog.GetLogProofParams) middleware.Responder { return tlog.NewGetLogProofOK().WithPayload(&consistencyProof) } -func inactiveShardLogInfo(ctx context.Context, tid int64, signer signature.Signer) (*models.InactiveShardLogInfo, error) { +func inactiveShardLogInfo(ctx context.Context, tid int64, cachedCheckpoints map[int64]string) (*models.InactiveShardLogInfo, error) { tc := trillianclient.NewTrillianClient(ctx, api.logClient, tid) resp := tc.GetLatest(0) if resp.Status != codes.OK { @@ -184,16 +183,11 @@ func inactiveShardLogInfo(ctx context.Context, tid int64, signer signature.Signe hashString := hex.EncodeToString(root.RootHash) treeSize := int64(root.TreeSize) - scBytes, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), tid, root.TreeSize, root.RootHash, signer) - if err != nil { - return nil, err - } - m := models.InactiveShardLogInfo{ RootHash: &hashString, TreeSize: &treeSize, TreeID: stringPointer(fmt.Sprintf("%d", tid)), - SignedTreeHead: stringPointer(string(scBytes)), + SignedTreeHead: stringPointer(cachedCheckpoints[tid]), } return &m, nil } diff --git a/tests/sharding-e2e-test.sh b/tests/sharding-e2e-test.sh index 71a3d2b46..61865aabd 100755 --- a/tests/sharding-e2e-test.sh +++ b/tests/sharding-e2e-test.sh @@ -63,6 +63,18 @@ function stringsMatch () { fi } +function stringsNotMatch () { + one=$1 + two=$2 + + if [[ "$one" != "$two" ]]; then + echo "Strings do not match" + else + echo "Strings $one match but shouldn't" + exit 1 + fi +} + function waitForRekorServer () { count=0 @@ -278,4 +290,14 @@ echo echo "Testing rekor-cli verification via Entry ID..." DEBUG=1 $REKOR_CLI verify --uuid $ENTRY_ID_1 --rekor_server http://localhost:3000 +# Verify that the checkpoint/SignedTreeHead for inactive shards is cached between calls +ACTIVE_SHARD_CHECKPOINT=$(curl "http://localhost:3000/api/v1/log" | jq .signedTreeHead | base64 -w 0) +INACTIVE_SHARD_CHECKPOINT=$(curl "http://localhost:3000/api/v1/log" | jq .inactiveShards[0].signedTreeHead | base64 -w 0) +ACTIVE_SHARD_CHECKPOINT_NOT_CACHED=$(curl "http://localhost:3000/api/v1/log" | jq .signedTreeHead | base64 -w 0) +INACTIVE_SHARD_CHECKPOINT_CACHED=$(curl "http://localhost:3000/api/v1/log" | jq .inactiveShards[0].signedTreeHead | base64 -w 0) +// inactive shard checkpoint is cached +stringsMatch $INACTIVE_SHARD_CHECKPOINT $INACTIVE_SHARD_CHECKPOINT_CACHED +// active shard checkpoint is not cached +stringsNotMatch $ACTIVE_SHARD_CHECKPOINT $ACTIVE_SHARD_CHECKPOINT_NOT_CACHED + echo "Test passed successfully :)"