From 2770fb8bb42aa8e4c50b8f67b3029865b00fc8aa Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Tue, 30 Aug 2022 01:34:07 +0000 Subject: [PATCH 1/5] Include checkpoint (STH) in entry upload and retrieve responses This associates a root hash in an inclusion proof with a signed commitment from the log. Previously, without this included, there was no connection between an inclusion proof and the log. An inclusion proof and checkpoint can be an alternative proof of inclusion instead of a SET. Ref #988 Signed-off-by: Hayden Blauzvern --- cmd/rekor-cli/app/verify.go | 18 ++++--- openapi.yaml | 5 ++ pkg/api/entries.go | 68 +++++++++++++++++++++++-- pkg/api/trillian_client.go | 2 + pkg/generated/models/inclusion_proof.go | 17 +++++++ pkg/generated/restapi/embedded_spec.go | 16 +++++- tests/e2e_test.go | 2 + 7 files changed, 116 insertions(+), 12 deletions(-) diff --git a/cmd/rekor-cli/app/verify.go b/cmd/rekor-cli/app/verify.go index 85293174a..1943730d3 100644 --- a/cmd/rekor-cli/app/verify.go +++ b/cmd/rekor-cli/app/verify.go @@ -37,18 +37,24 @@ import ( ) type verifyCmdOutput struct { - RootHash string - EntryUUID string - Index int64 - Size int64 - Hashes []string + RootHash string + EntryUUID string + Index int64 + Size int64 + Hashes []string + Checkpoint string } func (v *verifyCmdOutput) String() string { s := fmt.Sprintf("Current Root Hash: %v\n", v.RootHash) s += fmt.Sprintf("Entry Hash: %v\n", v.EntryUUID) s += fmt.Sprintf("Entry Index: %v\n", v.Index) - s += fmt.Sprintf("Current Tree Size: %v\n\n", v.Size) + s += fmt.Sprintf("Current Tree Size: %v\n", v.Size) + if len(v.Checkpoint) > 0 { + s += fmt.Sprintf("Checkpoint:\n%v\n\n", v.Checkpoint) + } else { + s += "\n" + } s += "Inclusion Proof:\n" hasher := rfc6962.DefaultHasher diff --git a/openapi.yaml b/openapi.yaml index 93af089b6..d6311e302 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -631,11 +631,16 @@ definitions: type: string description: SHA256 hash value expressed in hexadecimal format pattern: '^[0-9a-fA-F]{64}$' + checkpoint: + type: string + format: signedCheckpoint + description: The checkpoint (signed tree head) that the inclusion proof is based on required: - logIndex - rootHash - treeSize - hashes + - checkpoint Error: type: object diff --git a/pkg/api/entries.go b/pkg/api/entries.go index c16220389..75e20b460 100644 --- a/pkg/api/entries.go +++ b/pkg/api/entries.go @@ -42,6 +42,7 @@ import ( "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/sharding" "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/rekor/pkg/util" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" ) @@ -92,11 +93,32 @@ func logEntryFromLeaf(ctx context.Context, signer signature.Signer, tc TrillianC return nil, fmt.Errorf("signing entry error: %w", err) } + // sign a checkpoint as a commitment to the current root hash + sth, err := util.CreateSignedCheckpoint(util.Checkpoint{ + Origin: fmt.Sprintf("%s - %d", viper.GetString("rekor_server.hostname"), tc.logID), + Size: root.TreeSize, + Hash: root.RootHash, + }) + if err != nil { + return nil, fmt.Errorf("error marshalling checkpoint: %w", err) + } + sth.SetTimestamp(uint64(*logEntryAnon.IntegratedTime)) + _, err = sth.Sign(viper.GetString("rekor_server.hostname"), api.signer, options.WithContext(ctx)) + if err != nil { + return nil, fmt.Errorf("error signing checkpoint: %w", err) + } + scBytes, err := sth.SignedNote.MarshalText() + if err != nil { + return nil, fmt.Errorf("error marshalling checkpoint: %w", err) + } + scString := string(scBytes) + inclusionProof := models.InclusionProof{ - TreeSize: swag.Int64(int64(root.TreeSize)), - RootHash: swag.String(hex.EncodeToString(root.RootHash)), - LogIndex: swag.Int64(proof.GetLeafIndex()), - Hashes: hashes, + TreeSize: swag.Int64(int64(root.TreeSize)), + RootHash: swag.String(hex.EncodeToString(root.RootHash)), + LogIndex: swag.Int64(proof.GetLeafIndex()), + Hashes: hashes, + Checkpoint: &scString, } uuid := hex.EncodeToString(leaf.MerkleLeafHash) @@ -261,7 +283,45 @@ func createLogEntry(params entries.CreateLogEntryParams) (models.LogEntry, middl return nil, handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("signing entry error: %v", err), signingError) } + root := &ttypes.LogRootV1{} + if err := root.UnmarshalBinary(resp.getLeafAndProofResult.SignedLogRoot.LogRoot); err != nil { + return nil, handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("error unmarshalling log root: %v", err), sthGenerateError) + } + hashes := []string{} + for _, hash := range resp.getLeafAndProofResult.Proof.Hashes { + hashes = append(hashes, hex.EncodeToString(hash)) + } + + // sign a checkpoint as a commitment to the current root hash + sth, err := util.CreateSignedCheckpoint(util.Checkpoint{ + Origin: fmt.Sprintf("%s - %d", viper.GetString("rekor_server.hostname"), tc.logID), + Size: root.TreeSize, + Hash: root.RootHash, + }) + if err != nil { + return nil, handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("error creating checkpoint: %v", err), sthGenerateError) + } + sth.SetTimestamp(uint64(*logEntryAnon.IntegratedTime)) + _, err = sth.Sign(viper.GetString("rekor_server.hostname"), api.signer, options.WithContext(ctx)) + if err != nil { + return nil, handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("error signing checkpoint: %v", err), sthGenerateError) + } + scBytes, err := sth.SignedNote.MarshalText() + if err != nil { + return nil, handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("error marshalling checkpoint: %v", err), sthGenerateError) + } + scString := string(scBytes) + + inclusionProof := models.InclusionProof{ + TreeSize: swag.Int64(int64(root.TreeSize)), + RootHash: swag.String(hex.EncodeToString(root.RootHash)), + LogIndex: swag.Int64(queuedLeaf.LeafIndex), + Hashes: hashes, + Checkpoint: &scString, + } + logEntryAnon.Verification = &models.LogEntryAnonVerification{ + InclusionProof: &inclusionProof, SignedEntryTimestamp: strfmt.Base64(signature), } diff --git a/pkg/api/trillian_client.go b/pkg/api/trillian_client.go index cf625aa16..6e9278d8b 100644 --- a/pkg/api/trillian_client.go +++ b/pkg/api/trillian_client.go @@ -185,6 +185,8 @@ func (t *TrillianClient) addLeaf(byteValue []byte) *Response { status: status.Code(err), err: err, getAddResult: resp, + // include getLeafAndProofResult for inclusion proof + getLeafAndProofResult: leafResp.getLeafAndProofResult, } } diff --git a/pkg/generated/models/inclusion_proof.go b/pkg/generated/models/inclusion_proof.go index 61399816c..86f0d7b94 100644 --- a/pkg/generated/models/inclusion_proof.go +++ b/pkg/generated/models/inclusion_proof.go @@ -36,6 +36,10 @@ import ( // swagger:model InclusionProof type InclusionProof struct { + // The checkpoint (signed tree head) that the inclusion proof is based on + // Required: true + Checkpoint *string `json:"checkpoint"` + // A list of hashes required to compute the inclusion proof, sorted in order from leaf to root // Required: true Hashes []string `json:"hashes"` @@ -60,6 +64,10 @@ type InclusionProof struct { func (m *InclusionProof) Validate(formats strfmt.Registry) error { var res []error + if err := m.validateCheckpoint(formats); err != nil { + res = append(res, err) + } + if err := m.validateHashes(formats); err != nil { res = append(res, err) } @@ -82,6 +90,15 @@ func (m *InclusionProof) Validate(formats strfmt.Registry) error { return nil } +func (m *InclusionProof) validateCheckpoint(formats strfmt.Registry) error { + + if err := validate.Required("checkpoint", "body", m.Checkpoint); err != nil { + return err + } + + return nil +} + func (m *InclusionProof) validateHashes(formats strfmt.Registry) error { if err := validate.Required("hashes", "body", m.Hashes); err != nil { diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go index 708261d4e..ad13f3fd5 100644 --- a/pkg/generated/restapi/embedded_spec.go +++ b/pkg/generated/restapi/embedded_spec.go @@ -432,9 +432,15 @@ func init() { "logIndex", "rootHash", "treeSize", - "hashes" + "hashes", + "checkpoint" ], "properties": { + "checkpoint": { + "description": "The checkpoint (signed tree head) that the inclusion proof is based on", + "type": "string", + "format": "signedCheckpoint" + }, "hashes": { "description": "A list of hashes required to compute the inclusion proof, sorted in order from leaf to root", "type": "array", @@ -1831,9 +1837,15 @@ func init() { "logIndex", "rootHash", "treeSize", - "hashes" + "hashes", + "checkpoint" ], "properties": { + "checkpoint": { + "description": "The checkpoint (signed tree head) that the inclusion proof is based on", + "type": "string", + "format": "signedCheckpoint" + }, "hashes": { "description": "A list of hashes required to compute the inclusion proof, sorted in order from leaf to root", "type": "array", diff --git a/tests/e2e_test.go b/tests/e2e_test.go index 34d018b42..0cd9ec732 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -154,6 +154,7 @@ func TestUploadVerifyRekord(t *testing.T) { // Now we should be able to verify it. out = runCli(t, "verify", "--artifact", artifactPath, "--signature", sigPath, "--public-key", pubPath) outputContains(t, out, "Inclusion Proof:") + outputContains(t, out, "Checkpoint:") } func TestUploadVerifyHashedRekord(t *testing.T) { @@ -183,6 +184,7 @@ func TestUploadVerifyHashedRekord(t *testing.T) { // Now we should be able to verify it. out = runCli(t, "verify", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath) outputContains(t, out, "Inclusion Proof:") + outputContains(t, out, "Checkpoint:") } func TestUploadVerifyRpm(t *testing.T) { From c0c8c1c94ac7e94728911ae00bbad2652ebf70e4 Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Thu, 1 Sep 2022 00:01:52 +0000 Subject: [PATCH 2/5] Refactor with common implementation for creating signed checkpoint Signed-off-by: Hayden Blauzvern --- pkg/api/entries.go | 42 ++++++------------------------------- pkg/api/tlog.go | 42 +++++-------------------------------- pkg/util/checkpoint.go | 27 ++++++++++++++++++++++++ pkg/util/checkpoint_test.go | 39 ++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 73 deletions(-) diff --git a/pkg/api/entries.go b/pkg/api/entries.go index 75e20b460..997c7ed4a 100644 --- a/pkg/api/entries.go +++ b/pkg/api/entries.go @@ -93,32 +93,17 @@ func logEntryFromLeaf(ctx context.Context, signer signature.Signer, tc TrillianC return nil, fmt.Errorf("signing entry error: %w", err) } - // sign a checkpoint as a commitment to the current root hash - sth, err := util.CreateSignedCheckpoint(util.Checkpoint{ - Origin: fmt.Sprintf("%s - %d", viper.GetString("rekor_server.hostname"), tc.logID), - Size: root.TreeSize, - Hash: root.RootHash, - }) + scBytes, err := util.CreateAndSignCheckpoint(viper.GetString("rekor_server.hostname"), tc.logID, root, api.signer, ctx) if err != nil { - return nil, fmt.Errorf("error marshalling checkpoint: %w", err) - } - sth.SetTimestamp(uint64(*logEntryAnon.IntegratedTime)) - _, err = sth.Sign(viper.GetString("rekor_server.hostname"), api.signer, options.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("error signing checkpoint: %w", err) - } - scBytes, err := sth.SignedNote.MarshalText() - if err != nil { - return nil, fmt.Errorf("error marshalling checkpoint: %w", err) + return nil, err } - scString := string(scBytes) inclusionProof := models.InclusionProof{ TreeSize: swag.Int64(int64(root.TreeSize)), RootHash: swag.String(hex.EncodeToString(root.RootHash)), LogIndex: swag.Int64(proof.GetLeafIndex()), Hashes: hashes, - Checkpoint: &scString, + Checkpoint: stringPointer(string(scBytes)), } uuid := hex.EncodeToString(leaf.MerkleLeafHash) @@ -292,32 +277,17 @@ func createLogEntry(params entries.CreateLogEntryParams) (models.LogEntry, middl hashes = append(hashes, hex.EncodeToString(hash)) } - // sign a checkpoint as a commitment to the current root hash - sth, err := util.CreateSignedCheckpoint(util.Checkpoint{ - Origin: fmt.Sprintf("%s - %d", viper.GetString("rekor_server.hostname"), tc.logID), - Size: root.TreeSize, - Hash: root.RootHash, - }) - if err != nil { - return nil, handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("error creating checkpoint: %v", err), sthGenerateError) - } - sth.SetTimestamp(uint64(*logEntryAnon.IntegratedTime)) - _, err = sth.Sign(viper.GetString("rekor_server.hostname"), api.signer, options.WithContext(ctx)) - if err != nil { - return nil, handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("error signing checkpoint: %v", err), sthGenerateError) - } - scBytes, err := sth.SignedNote.MarshalText() + scBytes, err := util.CreateAndSignCheckpoint(viper.GetString("rekor_server.hostname"), tc.logID, root, api.signer, ctx) if err != nil { - return nil, handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("error marshalling checkpoint: %v", err), sthGenerateError) + return nil, handleRekorAPIError(params, http.StatusInternalServerError, err, sthGenerateError) } - scString := string(scBytes) inclusionProof := models.InclusionProof{ TreeSize: swag.Int64(int64(root.TreeSize)), RootHash: swag.String(hex.EncodeToString(root.RootHash)), LogIndex: swag.Int64(queuedLeaf.LeafIndex), Hashes: hashes, - Checkpoint: &scString, + Checkpoint: stringPointer(string(scBytes)), } logEntryAnon.Verification = &models.LogEntryAnonVerification{ diff --git a/pkg/api/tlog.go b/pkg/api/tlog.go index c5993dc9e..22532da44 100644 --- a/pkg/api/tlog.go +++ b/pkg/api/tlog.go @@ -21,7 +21,6 @@ import ( "fmt" "net/http" "strconv" - "time" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/swag" @@ -33,7 +32,6 @@ import ( "github.com/sigstore/rekor/pkg/generated/restapi/operations/tlog" "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/util" - "github.com/sigstore/sigstore/pkg/signature/options" ) // GetLogInfoHandler returns the current size of the tree and the STH @@ -68,32 +66,16 @@ func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder { hashString := hex.EncodeToString(root.RootHash) treeSize := int64(root.TreeSize) - sth, err := util.CreateSignedCheckpoint(util.Checkpoint{ - Origin: fmt.Sprintf("%s - %d", viper.GetString("rekor_server.hostname"), tc.ranges.ActiveTreeID()), - Size: root.TreeSize, - Hash: root.RootHash, - }) + scBytes, err := util.CreateAndSignCheckpoint(viper.GetString("rekor_server.hostname"), + tc.ranges.ActiveTreeID(), root, api.signer, params.HTTPRequest.Context()) if err != nil { - return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("marshalling error: %w", err), sthGenerateError) + return handleRekorAPIError(params, http.StatusInternalServerError, err, sthGenerateError) } - sth.SetTimestamp(uint64(time.Now().UnixNano())) - - // sign the log root ourselves to get the log root signature - _, err = sth.Sign(viper.GetString("rekor_server.hostname"), api.signer, options.WithContext(params.HTTPRequest.Context())) - if err != nil { - return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("signing error: %w", err), signingError) - } - - scBytes, err := sth.SignedNote.MarshalText() - if err != nil { - return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("marshalling error: %w", err), sthGenerateError) - } - scString := string(scBytes) logInfo := models.LogInfo{ RootHash: &hashString, TreeSize: &treeSize, - SignedTreeHead: &scString, + SignedTreeHead: stringPointer(string(scBytes)), TreeID: stringPointer(fmt.Sprintf("%d", tc.logID)), InactiveShards: inactiveShards, } @@ -169,25 +151,11 @@ func inactiveShardLogInfo(ctx context.Context, tid int64) (*models.InactiveShard hashString := hex.EncodeToString(root.RootHash) treeSize := int64(root.TreeSize) - sth, err := util.CreateSignedCheckpoint(util.Checkpoint{ - Origin: fmt.Sprintf("%s - %d", viper.GetString("rekor_server.hostname"), tid), - Size: root.TreeSize, - Hash: root.RootHash, - }) + scBytes, err := util.CreateAndSignCheckpoint(viper.GetString("rekor_server.hostname"), tid, root, api.signer, ctx) 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 - } - - scBytes, err := sth.SignedNote.MarshalText() - if err != nil { - return nil, err - } m := models.InactiveShardLogInfo{ RootHash: &hashString, TreeSize: &treeSize, diff --git a/pkg/util/checkpoint.go b/pkg/util/checkpoint.go index efa7b8bb1..0f4834fd1 100644 --- a/pkg/util/checkpoint.go +++ b/pkg/util/checkpoint.go @@ -17,11 +17,17 @@ package util import ( "bytes" + "context" "encoding/base64" "errors" "fmt" "strconv" "strings" + "time" + + "github.com/google/trillian/types" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" ) // heavily borrowed from https://github.com/google/trillian-examples/blob/master/formats/log/checkpoint.go @@ -160,3 +166,24 @@ func (r *SignedCheckpoint) GetTimestamp() uint64 { } return ts } + +// CreateAndSignCheckpoint creates a signed checkpoint as a commitment to the current root hash +func CreateAndSignCheckpoint(hostname string, treeID int64, root *types.LogRootV1, signer signature.Signer, ctx context.Context) ([]byte, error) { + sth, err := CreateSignedCheckpoint(Checkpoint{ + Origin: fmt.Sprintf("%s - %d", hostname, treeID), + Size: root.TreeSize, + Hash: root.RootHash, + }) + if err != nil { + return []byte{}, fmt.Errorf("error creating checkpoint: %v", err) + } + sth.SetTimestamp(uint64(time.Now().UnixNano())) + if _, err := sth.Sign(hostname, signer, options.WithContext(ctx)); err != nil { + return []byte{}, fmt.Errorf("error signing checkpoint: %v", err) + } + scBytes, err := sth.SignedNote.MarshalText() + if err != nil { + return []byte{}, fmt.Errorf("error marshalling checkpoint: %v", err) + } + return scBytes, nil +} diff --git a/pkg/util/checkpoint_test.go b/pkg/util/checkpoint_test.go index 008c0aa1e..516a2c8b9 100644 --- a/pkg/util/checkpoint_test.go +++ b/pkg/util/checkpoint_test.go @@ -16,16 +16,21 @@ package util import ( + "bytes" + "context" "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/sha256" + "fmt" "testing" "time" "github.com/google/go-cmp/cmp" + "github.com/google/trillian/types" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" "golang.org/x/mod/sumdb/note" @@ -442,3 +447,37 @@ func TestUnmarshalSignedCheckpoint(t *testing.T) { }) } } + +func TestSignCheckpoint(t *testing.T) { + hostname := "rekor.localhost" + treeID := int64(123) + rootHash := sha256.Sum256([]byte{1, 2, 3}) + treeSize := uint64(42) + signer, _, err := signature.NewDefaultECDSASignerVerifier() + if err != nil { + t.Fatalf("error generating signer: %v", err) + } + ctx := context.Background() + scBytes, err := CreateAndSignCheckpoint(hostname, treeID, &types.LogRootV1{TreeSize: treeSize, RootHash: rootHash[:]}, signer, ctx) + if err != nil { + t.Fatalf("error creating signed checkpoint: %v", err) + } + + sth := SignedCheckpoint{} + if err := sth.UnmarshalText(scBytes); err != nil { + t.Fatalf("error unmarshalling signed checkpoint: %v", err) + } + if !sth.Verify(signer) { + t.Fatalf("checkpoint signature invalid") + } + expectedOrigin := fmt.Sprintf("%s - %d", hostname, treeID) + if sth.Origin != fmt.Sprintf("%s - %d", hostname, treeID) { + t.Fatalf("unexpected origin: got %s, expected %s", expectedOrigin, sth.Origin) + } + if !bytes.Equal(sth.Hash, rootHash[:]) { + t.Fatalf("unexpected mismatch of root hash") + } + if sth.Size != treeSize { + t.Fatalf("unexpected tree size: got %d, expected %d", sth.Size, treeSize) + } +} From 5df9779e45d529368f6af05f95964f0e5c3a9df4 Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Thu, 1 Sep 2022 00:25:45 +0000 Subject: [PATCH 3/5] Update client to verify checkpoint signature Signed-off-by: Hayden Blauzvern --- cmd/rekor-cli/app/get.go | 7 +++++++ cmd/rekor-cli/app/upload.go | 10 ++++++++++ cmd/rekor-cli/app/verify.go | 10 ++++++++++ pkg/verify/verify.go | 22 ++++++++++++++++++---- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/cmd/rekor-cli/app/get.go b/cmd/rekor-cli/app/get.go index 16da04c94..c2ea97cd9 100644 --- a/cmd/rekor-cli/app/get.go +++ b/cmd/rekor-cli/app/get.go @@ -118,6 +118,13 @@ var getCmd = &cobra.Command{ return nil, fmt.Errorf("unable to verify entry was added to log: %w", err) } + // verify checkpoint + if entry.Verification.InclusionProof.Checkpoint != nil { + if err := verify.VerifyCheckpointSignature(&entry, verifier); err != nil { + return nil, err + } + } + return parseEntry(ix, entry) } } diff --git a/cmd/rekor-cli/app/upload.go b/cmd/rekor-cli/app/upload.go index f504072db..da78fb2f9 100644 --- a/cmd/rekor-cli/app/upload.go +++ b/cmd/rekor-cli/app/upload.go @@ -141,6 +141,16 @@ var uploadCmd = &cobra.Command{ if err := verify.VerifySignedEntryTimestamp(ctx, &logEntry, verifier); err != nil { return nil, fmt.Errorf("unable to verify entry was added to log: %w", err) } + if logEntry.Verification.InclusionProof != nil { + // verify inclusion proof + if err := verify.VerifyInclusion(ctx, &logEntry); err != nil { + return nil, fmt.Errorf("error verifying inclusion proof: %w", err) + } + // verify checkpoint + if err := verify.VerifyCheckpointSignature(&logEntry, verifier); err != nil { + return nil, err + } + } return &uploadCmdOutput{ Location: string(resp.Location), diff --git a/cmd/rekor-cli/app/verify.go b/cmd/rekor-cli/app/verify.go index 1943730d3..1c8c2e225 100644 --- a/cmd/rekor-cli/app/verify.go +++ b/cmd/rekor-cli/app/verify.go @@ -153,6 +153,9 @@ var verifyCmd = &cobra.Command{ Size: *v.Verification.InclusionProof.TreeSize, Hashes: v.Verification.InclusionProof.Hashes, } + if v.Verification.InclusionProof.Checkpoint != nil { + o.Checkpoint = *v.Verification.InclusionProof.Checkpoint + } entry = v } @@ -177,6 +180,13 @@ var verifyCmd = &cobra.Command{ return nil, fmt.Errorf("validating entry: %w", err) } + // verify checkpoint + if entry.Verification.InclusionProof.Checkpoint != nil { + if err := verify.VerifyCheckpointSignature(&entry, verifier); err != nil { + return nil, err + } + } + return o, err }), } diff --git a/pkg/verify/verify.go b/pkg/verify/verify.go index 516aed339..f3cbf94df 100644 --- a/pkg/verify/verify.go +++ b/pkg/verify/verify.go @@ -75,7 +75,7 @@ func ProveConsistency(ctx context.Context, rClient *client.Rekor, // VerifyCurrentCheckpoint verifies the provided checkpoint by verifying consistency // against a newly fetched Checkpoint. -//nolint +// nolint func VerifyCurrentCheckpoint(ctx context.Context, rClient *client.Rekor, verifier signature.Verifier, oldSTH *util.SignedCheckpoint) (*util.SignedCheckpoint, error) { // The oldSTH should already be verified, but check for robustness. @@ -108,10 +108,24 @@ func VerifyCurrentCheckpoint(ctx context.Context, rClient *client.Rekor, verifie return &sth, nil } +// VerifyCheckpointSignature verifies the signature on a checkpoint (signed tree head). It does +// not verify consistency against other checkpoints. +// nolint +func VerifyCheckpointSignature(e *models.LogEntryAnon, verifier signature.Verifier) error { + sth := &util.SignedCheckpoint{} + if err := sth.UnmarshalText([]byte(*e.Verification.InclusionProof.Checkpoint)); err != nil { + return fmt.Errorf("unmarshalling log entry checkpoint to SignedCheckpoint: %w", err) + } + if !sth.Verify(verifier) { + return errors.New("signature on checkpoint did not verify") + } + return nil +} + // VerifyInclusion verifies an entry's inclusion proof. Clients MUST either verify // the root hash against a new STH (via VerifyCurrentCheckpoint) or against a // trusted, existing STH (via ProveConsistency). -//nolint +// nolint func VerifyInclusion(ctx context.Context, e *models.LogEntryAnon) error { if e.Verification == nil || e.Verification.InclusionProof == nil { return errors.New("inclusion proof not provided") @@ -145,7 +159,7 @@ func VerifyInclusion(ctx context.Context, e *models.LogEntryAnon) error { // VerifySignedEntryTimestamp verifies the entry's SET against the provided // public key. -//nolint +// nolint func VerifySignedEntryTimestamp(ctx context.Context, e *models.LogEntryAnon, verifier signature.Verifier) error { if e.Verification == nil { return fmt.Errorf("missing verification") @@ -187,7 +201,7 @@ func VerifySignedEntryTimestamp(ctx context.Context, e *models.LogEntryAnon, ver // VerifyLogEntry performs verification of a LogEntry given a Rekor verifier. // Performs inclusion proof verification up to a verified root hash and // SignedEntryTimestamp verification. -//nolint +// nolint func VerifyLogEntry(ctx context.Context, e *models.LogEntryAnon, verifier signature.Verifier) error { // Verify the inclusion proof using the body's leaf hash. if err := VerifyInclusion(ctx, e); err != nil { From 311a1d3f35ad892ea26896ab385092e3b44a0dea Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Thu, 1 Sep 2022 00:31:07 +0000 Subject: [PATCH 4/5] Address linter Signed-off-by: Hayden Blauzvern --- cmd/rekor-cli/app/get.go | 2 +- pkg/api/entries.go | 4 ++-- pkg/api/tlog.go | 6 +++--- pkg/util/checkpoint.go | 2 +- pkg/util/checkpoint_test.go | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/rekor-cli/app/get.go b/cmd/rekor-cli/app/get.go index c2ea97cd9..90d59dffc 100644 --- a/cmd/rekor-cli/app/get.go +++ b/cmd/rekor-cli/app/get.go @@ -120,7 +120,7 @@ var getCmd = &cobra.Command{ // verify checkpoint if entry.Verification.InclusionProof.Checkpoint != nil { - if err := verify.VerifyCheckpointSignature(&entry, verifier); err != nil { + if err := verify.VerifyCheckpointSignature(&e, verifier); err != nil { return nil, err } } diff --git a/pkg/api/entries.go b/pkg/api/entries.go index 997c7ed4a..c4d1e50c4 100644 --- a/pkg/api/entries.go +++ b/pkg/api/entries.go @@ -93,7 +93,7 @@ func logEntryFromLeaf(ctx context.Context, signer signature.Signer, tc TrillianC return nil, fmt.Errorf("signing entry error: %w", err) } - scBytes, err := util.CreateAndSignCheckpoint(viper.GetString("rekor_server.hostname"), tc.logID, root, api.signer, ctx) + scBytes, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), tc.logID, root, api.signer) if err != nil { return nil, err } @@ -277,7 +277,7 @@ func createLogEntry(params entries.CreateLogEntryParams) (models.LogEntry, middl hashes = append(hashes, hex.EncodeToString(hash)) } - scBytes, err := util.CreateAndSignCheckpoint(viper.GetString("rekor_server.hostname"), tc.logID, root, api.signer, ctx) + scBytes, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), tc.logID, root, api.signer) if err != nil { return nil, handleRekorAPIError(params, http.StatusInternalServerError, err, sthGenerateError) } diff --git a/pkg/api/tlog.go b/pkg/api/tlog.go index 22532da44..33e7d699e 100644 --- a/pkg/api/tlog.go +++ b/pkg/api/tlog.go @@ -66,8 +66,8 @@ func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder { hashString := hex.EncodeToString(root.RootHash) treeSize := int64(root.TreeSize) - scBytes, err := util.CreateAndSignCheckpoint(viper.GetString("rekor_server.hostname"), - tc.ranges.ActiveTreeID(), root, api.signer, params.HTTPRequest.Context()) + scBytes, err := util.CreateAndSignCheckpoint(params.HTTPRequest.Context(), + viper.GetString("rekor_server.hostname"), tc.ranges.ActiveTreeID(), root, api.signer) if err != nil { return handleRekorAPIError(params, http.StatusInternalServerError, err, sthGenerateError) } @@ -151,7 +151,7 @@ func inactiveShardLogInfo(ctx context.Context, tid int64) (*models.InactiveShard hashString := hex.EncodeToString(root.RootHash) treeSize := int64(root.TreeSize) - scBytes, err := util.CreateAndSignCheckpoint(viper.GetString("rekor_server.hostname"), tid, root, api.signer, ctx) + scBytes, err := util.CreateAndSignCheckpoint(ctx, viper.GetString("rekor_server.hostname"), tid, root, api.signer) if err != nil { return nil, err } diff --git a/pkg/util/checkpoint.go b/pkg/util/checkpoint.go index 0f4834fd1..f0835d4cb 100644 --- a/pkg/util/checkpoint.go +++ b/pkg/util/checkpoint.go @@ -168,7 +168,7 @@ func (r *SignedCheckpoint) GetTimestamp() uint64 { } // CreateAndSignCheckpoint creates a signed checkpoint as a commitment to the current root hash -func CreateAndSignCheckpoint(hostname string, treeID int64, root *types.LogRootV1, signer signature.Signer, ctx context.Context) ([]byte, error) { +func CreateAndSignCheckpoint(ctx context.Context, hostname string, treeID int64, root *types.LogRootV1, signer signature.Signer) ([]byte, error) { sth, err := CreateSignedCheckpoint(Checkpoint{ Origin: fmt.Sprintf("%s - %d", hostname, treeID), Size: root.TreeSize, diff --git a/pkg/util/checkpoint_test.go b/pkg/util/checkpoint_test.go index 516a2c8b9..d78d6d5ff 100644 --- a/pkg/util/checkpoint_test.go +++ b/pkg/util/checkpoint_test.go @@ -458,7 +458,7 @@ func TestSignCheckpoint(t *testing.T) { t.Fatalf("error generating signer: %v", err) } ctx := context.Background() - scBytes, err := CreateAndSignCheckpoint(hostname, treeID, &types.LogRootV1{TreeSize: treeSize, RootHash: rootHash[:]}, signer, ctx) + scBytes, err := CreateAndSignCheckpoint(ctx, hostname, treeID, &types.LogRootV1{TreeSize: treeSize, RootHash: rootHash[:]}, signer) if err != nil { t.Fatalf("error creating signed checkpoint: %v", err) } From 058fcdbc2ed4703d64e9ab852acc47b848303ee1 Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Thu, 1 Sep 2022 16:24:16 +0000 Subject: [PATCH 5/5] Add checkpoint verification to VerifyLogEntry Signed-off-by: Hayden Blauzvern --- cmd/rekor-cli/app/upload.go | 1 + cmd/rekor-cli/app/verify.go | 8 +------- pkg/util/checkpoint.go | 6 +++--- pkg/verify/verify.go | 13 +++++++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cmd/rekor-cli/app/upload.go b/cmd/rekor-cli/app/upload.go index da78fb2f9..a5ec22340 100644 --- a/cmd/rekor-cli/app/upload.go +++ b/cmd/rekor-cli/app/upload.go @@ -141,6 +141,7 @@ var uploadCmd = &cobra.Command{ if err := verify.VerifySignedEntryTimestamp(ctx, &logEntry, verifier); err != nil { return nil, fmt.Errorf("unable to verify entry was added to log: %w", err) } + // TODO: Remove conditional once inclusion proof/checkpoint is always returned by server. if logEntry.Verification.InclusionProof != nil { // verify inclusion proof if err := verify.VerifyInclusion(ctx, &logEntry); err != nil { diff --git a/cmd/rekor-cli/app/verify.go b/cmd/rekor-cli/app/verify.go index 1c8c2e225..844b4bbc1 100644 --- a/cmd/rekor-cli/app/verify.go +++ b/cmd/rekor-cli/app/verify.go @@ -176,17 +176,11 @@ var verifyCmd = &cobra.Command{ return nil, err } + // verify inclusion proof, checkpoint, and SET if err := verify.VerifyLogEntry(ctx, &entry, verifier); err != nil { return nil, fmt.Errorf("validating entry: %w", err) } - // verify checkpoint - if entry.Verification.InclusionProof.Checkpoint != nil { - if err := verify.VerifyCheckpointSignature(&entry, verifier); err != nil { - return nil, err - } - } - return o, err }), } diff --git a/pkg/util/checkpoint.go b/pkg/util/checkpoint.go index f0835d4cb..ee6059e2b 100644 --- a/pkg/util/checkpoint.go +++ b/pkg/util/checkpoint.go @@ -175,15 +175,15 @@ func CreateAndSignCheckpoint(ctx context.Context, hostname string, treeID int64, Hash: root.RootHash, }) if err != nil { - return []byte{}, fmt.Errorf("error creating checkpoint: %v", err) + return nil, fmt.Errorf("error creating checkpoint: %v", err) } sth.SetTimestamp(uint64(time.Now().UnixNano())) if _, err := sth.Sign(hostname, signer, options.WithContext(ctx)); err != nil { - return []byte{}, fmt.Errorf("error signing checkpoint: %v", err) + return nil, fmt.Errorf("error signing checkpoint: %v", err) } scBytes, err := sth.SignedNote.MarshalText() if err != nil { - return []byte{}, fmt.Errorf("error marshalling checkpoint: %v", err) + return nil, fmt.Errorf("error marshalling checkpoint: %v", err) } return scBytes, nil } diff --git a/pkg/verify/verify.go b/pkg/verify/verify.go index f3cbf94df..d01dad8e8 100644 --- a/pkg/verify/verify.go +++ b/pkg/verify/verify.go @@ -199,8 +199,8 @@ func VerifySignedEntryTimestamp(ctx context.Context, e *models.LogEntryAnon, ver } // VerifyLogEntry performs verification of a LogEntry given a Rekor verifier. -// Performs inclusion proof verification up to a verified root hash and -// SignedEntryTimestamp verification. +// Performs inclusion proof verification up to a verified root hash, +// SignedEntryTimestamp verification, and checkpoint verification. // nolint func VerifyLogEntry(ctx context.Context, e *models.LogEntryAnon, verifier signature.Verifier) error { // Verify the inclusion proof using the body's leaf hash. @@ -208,9 +208,14 @@ func VerifyLogEntry(ctx context.Context, e *models.LogEntryAnon, verifier signat return err } - // TODO: If/when we return an STH in the response, verify that too, against an - // optional known STH as well. + // TODO: Add support for verifying consistency against an optional provided checkpoint. // See https://github.com/sigstore/rekor/issues/988 + // TODO: Remove conditional once checkpoint is always returned by server. + if e.Verification.InclusionProof.Checkpoint != nil { + if err := VerifyCheckpointSignature(e, verifier); err != nil { + return err + } + } // Verify the Signed Entry Timestamp. if err := VerifySignedEntryTimestamp(ctx, e, verifier); err != nil {