Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update loginfo API endpoint to return information about inactive shards #738

Merged
merged 3 commits into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions cmd/rekor-cli/app/log_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import (
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"time"

"github.com/go-openapi/swag"
"github.com/google/trillian/merkle/logverifier"
"github.com/google/trillian/merkle/rfc6962"
"github.com/pkg/errors"
rclient "github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/spf13/cobra"
"github.com/spf13/viper"

Expand Down Expand Up @@ -81,13 +82,20 @@ var logInfoCmd = &cobra.Command{

logInfo := result.GetPayload()

// Verify inactive shards
if err := verifyInactiveTrees(rekorClient, serverURL, logInfo.InactiveShards); err != nil {
return nil, err
}

// Verify the active tree
sth := util.SignedCheckpoint{}
signedTreeHead := swag.StringValue(logInfo.SignedTreeHead)
if err := sth.UnmarshalText([]byte(signedTreeHead)); err != nil {
return nil, err
}
treeID := swag.StringValue(logInfo.TreeID)

if err := verifyTree(rekorClient, signedTreeHead, serverURL); err != nil {
if err := verifyTree(rekorClient, signedTreeHead, serverURL, treeID); err != nil {
return nil, err
}

Expand All @@ -101,8 +109,27 @@ var logInfoCmd = &cobra.Command{
}),
}

func verifyTree(rekorClient *rclient.Rekor, signedTreeHead, serverURL string) error {
func verifyInactiveTrees(rekorClient *rclient.Rekor, serverURL string, inactiveShards []*models.InactiveShardLogInfo) error {
if inactiveShards == nil {
return nil
}
log.CliLogger.Infof("Validating inactive shards...")
for _, shard := range inactiveShards {
signedTreeHead := swag.StringValue(shard.SignedTreeHead)
treeID := swag.StringValue(shard.TreeID)
if err := verifyTree(rekorClient, signedTreeHead, serverURL, treeID); err != nil {
return errors.Wrapf(err, "verifying inactive shard with ID %s", treeID)
}
}
log.CliLogger.Infof("Successfully validated inactive shards")
return nil
}

func verifyTree(rekorClient *rclient.Rekor, signedTreeHead, serverURL, treeID string) error {
oldState := state.Load(serverURL)
if treeID != "" {
oldState = state.Load(treeID)
}
sth := util.SignedCheckpoint{}
if err := sth.UnmarshalText([]byte(signedTreeHead)); err != nil {
return err
Expand All @@ -115,19 +142,24 @@ func verifyTree(rekorClient *rclient.Rekor, signedTreeHead, serverURL string) er
return errors.New("signature on tree head did not verify")
}

if err := proveConsistency(rekorClient, oldState, sth); err != nil {
if err := proveConsistency(rekorClient, oldState, sth, treeID); err != nil {
return err
}

if viper.GetBool("store_tree_state") {
if treeID != "" {
if err := state.Dump(treeID, &sth); err != nil {
log.CliLogger.Infof("Unable to store previous state: %v", err)
}
}
if err := state.Dump(serverURL, &sth); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no treeID this will just dump from the URL, but if there is both a treeID and a URL, what happens? Does the URL only cover the active tree and only in the case of an older client? Or does some state get stored twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some state will get stored twice! But when reading from the file the client will always prefer the treeID over the URL.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay cool, yeah I see that in oldState. Just trying to understand the mechanism here - the URL covers all trees, right? What does dumping by treeID (if available) add?

Copy link
Contributor Author

@priyawadhwa priyawadhwa Mar 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we have two shards, they'll be accessed at the same URL (e.g. rekor.sigstore.dev). We can't store state for both of them, because the key by URL would be the same. The TreeID is unique for each shard, so we can store state for all shards at once if we use treeID as the key.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense, I see it is a hash map so we're giving it differentiated keys. I will try to get some output to see it for myself. Thanks for the explanation.

log.CliLogger.Infof("Unable to store previous state: %v", err)
}
}
return nil
}

func proveConsistency(rekorClient *rclient.Rekor, oldState *util.SignedCheckpoint, sth util.SignedCheckpoint) error {
func proveConsistency(rekorClient *rclient.Rekor, oldState *util.SignedCheckpoint, sth util.SignedCheckpoint, treeID string) error {
if oldState == nil {
log.CliLogger.Infof("No previous log state stored, unable to prove consistency")
return nil
Expand All @@ -140,6 +172,7 @@ func proveConsistency(rekorClient *rclient.Rekor, oldState *util.SignedCheckpoin
firstSize := int64(persistedSize)
params.FirstSize = &firstSize
params.LastSize = int64(sth.Size)
params.TreeID = &treeID
proof, err := rekorClient.Tlog.GetLogProof(params)
if err != nil {
return err
Expand Down
8 changes: 4 additions & 4 deletions cmd/rekor-cli/app/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (

type persistedState map[string]*util.SignedCheckpoint

func Dump(url string, sth *util.SignedCheckpoint) error {
func Dump(key string, sth *util.SignedCheckpoint) error {
rekorDir, err := getRekorDir()
if err != nil {
return err
Expand All @@ -38,7 +38,7 @@ func Dump(url string, sth *util.SignedCheckpoint) error {
if state == nil {
state = make(persistedState)
}
state[url] = sth
state[key] = sth

b, err := json.Marshal(&state)
if err != nil {
Expand Down Expand Up @@ -67,9 +67,9 @@ func loadStateFile() persistedState {
return result
}

func Load(url string) *util.SignedCheckpoint {
func Load(key string) *util.SignedCheckpoint {
if state := loadStateFile(); state != nil {
return state[url]
return state[key]
}
return nil
}
Expand Down
29 changes: 29 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,35 @@ definitions:
minItems: 1

LogInfo:
type: object
properties:
rootHash:
type: string
description: The current hash value stored at the root of the merkle tree
pattern: '^[0-9a-fA-F]{64}$'
treeSize:
type: integer
description: The current number of nodes in the merkle tree
minimum: 1
signedTreeHead:
type: string
format: signedCheckpoint
description: The current signed tree head
treeID:
type: string
description: The current treeID
pattern: '^[0-9]+$'
inactiveShards:
type: array
items:
$ref: '#/definitions/InactiveShardLogInfo'

required:
- rootHash
- treeSize
- signedTreeHead
- treeID
InactiveShardLogInfo:
type: object
properties:
rootHash:
Expand Down
1 change: 1 addition & 0 deletions pkg/api/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const (
failedToGenerateTimestampResponse = "Error generating timestamp response"
sthGenerateError = "Error generating signed tree head"
unsupportedPKIFormat = "The PKI format requested is not supported by this server"
unexpectedInactiveShardError = "Unexpected error communicating with inactive shard"
)

func errorMsg(message string, code int) *models.Error {
Expand Down
60 changes: 60 additions & 0 deletions pkg/api/tlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package api

import (
"context"
"encoding/hex"
"fmt"
"net/http"
Expand All @@ -39,6 +40,20 @@ import (
func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder {
tc := NewTrillianClient(params.HTTPRequest.Context())

// for each inactive shard, get the loginfo
var inactiveShards []*models.InactiveShardLogInfo
for _, shard := range tc.ranges.GetRanges() {
if shard.TreeID == tc.ranges.ActiveTreeID() {
break
}
// Get details for this inactive shard
is, err := inactiveShardLogInfo(params.HTTPRequest.Context(), shard.TreeID)
if err != nil {
return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("inactive shard error: %w", err), unexpectedInactiveShardError)
}
inactiveShards = append(inactiveShards, is)
}

resp := tc.getLatest(0)
if resp.status != codes.OK {
return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc error: %w", resp.err), trillianCommunicationError)
Expand Down Expand Up @@ -80,6 +95,7 @@ func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder {
TreeSize: &treeSize,
SignedTreeHead: &scString,
TreeID: stringPointer(fmt.Sprintf("%d", tc.logID)),
InactiveShards: inactiveShards,
}

return tlog.NewGetLogInfoOK().WithPayload(&logInfo)
Expand Down Expand Up @@ -136,3 +152,47 @@ func GetLogProofHandler(params tlog.GetLogProofParams) middleware.Responder {

return tlog.NewGetLogProofOK().WithPayload(&consistencyProof)
}

func inactiveShardLogInfo(ctx context.Context, tid int64) (*models.InactiveShardLogInfo, error) {
tc := NewTrillianClientFromTreeID(ctx, tid)
resp := tc.getLatest(0)
if resp.status != codes.OK {
return nil, fmt.Errorf("resp code is not OK")
priyawadhwa marked this conversation as resolved.
Show resolved Hide resolved
}
result := resp.getLatestResult

root := &types.LogRootV1{}
if err := root.UnmarshalBinary(result.SignedLogRoot.LogRoot); err != nil {
return nil, err
}

hashString := hex.EncodeToString(root.RootHash)
treeSize := int64(root.TreeSize)

sth, err := util.CreateSignedCheckpoint(util.Checkpoint{
Origin: "Rekor",
Size: root.TreeSize,
Hash: root.RootHash,
})
if err != nil {
return nil, err
}
sth.SetTimestamp(uint64(time.Now().UnixNano()))

// sign the log root ourselves to get the log root signature
if _, err := sth.Sign(viper.GetString("rekor_server.hostname"), api.signer, options.WithContext(ctx)); err != nil {
return nil, err
}
Comment on lines +182 to +185
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this get the log root signature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this calls the Sign function, which stores the signature in the struct:

s.Signatures = append(s.Signatures, signature)

To access it you would call sth.Signatures. Sorry if that wasn't clear, let me know if that doesn't answer your question!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, I think I get it, this enables us to get the signature later (not in this line of code) with sth.Signatures - the lack of clarity is just from me being less familiar here, I appreciate the explanation.


scBytes, err := sth.SignedNote.MarshalText()
if err != nil {
return nil, err
}
m := models.InactiveShardLogInfo{
RootHash: &hashString,
TreeSize: &treeSize,
TreeID: stringPointer(fmt.Sprintf("%d", tid)),
SignedTreeHead: stringPointer(string(scBytes)),
}
return &m, nil
}
3 changes: 3 additions & 0 deletions pkg/api/trillian_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/google/trillian/merkle/rfc6962"
"github.com/pkg/errors"
"github.com/sigstore/rekor/pkg/log"
"github.com/sigstore/rekor/pkg/sharding"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand All @@ -37,13 +38,15 @@ import (

type TrillianClient struct {
client trillian.TrillianLogClient
ranges sharding.LogRanges
logID int64
context context.Context
}

func NewTrillianClient(ctx context.Context) TrillianClient {
return TrillianClient{
client: api.logClient,
ranges: api.logRanges,
logID: api.logID,
context: ctx,
}
Expand Down
Loading