From 5159d01f9881088937e4553c80c2674cdb0872ae Mon Sep 17 00:00:00 2001 From: asraa Date: Tue, 30 Aug 2022 10:43:06 -0500 Subject: [PATCH] feat: add verification functions (#986) * feat: add verification functions Signed-off-by: Asra Ali --- cmd/rekor-cli/app/get.go | 21 +++- cmd/rekor-cli/app/log_info.go | 65 +++------- cmd/rekor-cli/app/upload.go | 51 +------- cmd/rekor-cli/app/verify.go | 32 ++--- pkg/verify/verify.go | 207 +++++++++++++++++++++++++++++++ pkg/verify/verify_test.go | 225 ++++++++++++++++++++++++++++++++++ 6 files changed, 481 insertions(+), 120 deletions(-) create mode 100644 pkg/verify/verify.go create mode 100644 pkg/verify/verify_test.go diff --git a/cmd/rekor-cli/app/get.go b/cmd/rekor-cli/app/get.go index b75f8a5b5..16da04c94 100644 --- a/cmd/rekor-cli/app/get.go +++ b/cmd/rekor-cli/app/get.go @@ -36,6 +36,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/verify" ) type getCmdOutput struct { @@ -79,6 +80,7 @@ var getCmd = &cobra.Command{ } }, Run: format.WrapCmd(func(args []string) (interface{}, error) { + ctx := context.Background() rekorClient, err := client.GetRekorClient(viper.GetString("rekor_server"), client.WithUserAgent(UserAgent())) if err != nil { return nil, err @@ -89,6 +91,11 @@ var getCmd = &cobra.Command{ if logIndex == "" && uuid == "" { return nil, errors.New("either --uuid or --log-index must be specified") } + // retrieve rekor pubkey for verification + verifier, err := loadVerifier(rekorClient) + if err != nil { + return nil, fmt.Errorf("retrieving rekor public key") + } if logIndex != "" { params := entries.NewGetLogEntryByIndexParams() @@ -103,9 +110,12 @@ var getCmd = &cobra.Command{ if err != nil { return nil, err } + var e models.LogEntryAnon for ix, entry := range resp.Payload { - if verified, err := verifyLogEntry(context.Background(), rekorClient, entry); err != nil || !verified { - return nil, fmt.Errorf("unable to verify entry was added to log %w", err) + // verify log entry + e = entry + if err := verify.VerifyLogEntry(ctx, &e, verifier); err != nil { + return nil, fmt.Errorf("unable to verify entry was added to log: %w", err) } return parseEntry(ix, entry) @@ -132,13 +142,16 @@ var getCmd = &cobra.Command{ return nil, err } + var e models.LogEntryAnon for k, entry := range resp.Payload { if k != u { continue } - if verified, err := verifyLogEntry(context.Background(), rekorClient, entry); err != nil || !verified { - return nil, fmt.Errorf("unable to verify entry was added to log %w", err) + // verify log entry + e = entry + if err := verify.VerifyLogEntry(ctx, &e, verifier); err != nil { + return nil, fmt.Errorf("unable to verify entry was added to log: %w", err) } return parseEntry(k, entry) diff --git a/cmd/rekor-cli/app/log_info.go b/cmd/rekor-cli/app/log_info.go index 78ce185f8..a456867bc 100644 --- a/cmd/rekor-cli/app/log_info.go +++ b/cmd/rekor-cli/app/log_info.go @@ -16,10 +16,9 @@ package app import ( - "bytes" + "context" "crypto" "crypto/x509" - "encoding/hex" "encoding/pem" "errors" "fmt" @@ -28,10 +27,10 @@ import ( "github.com/go-openapi/swag" rclient "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" + + "github.com/sigstore/rekor/pkg/verify" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/transparency-dev/merkle/proof" - "github.com/transparency-dev/merkle/rfc6962" "github.com/sigstore/rekor/cmd/rekor-cli/app/format" "github.com/sigstore/rekor/cmd/rekor-cli/app/state" @@ -70,6 +69,7 @@ var logInfoCmd = &cobra.Command{ Long: `Prints info about the transparency log`, Run: format.WrapCmd(func(args []string) (interface{}, error) { serverURL := viper.GetString("rekor_server") + ctx := context.Background() rekorClient, err := client.GetRekorClient(serverURL, client.WithUserAgent(UserAgent())) if err != nil { return nil, err @@ -85,7 +85,7 @@ var logInfoCmd = &cobra.Command{ logInfo := result.GetPayload() // Verify inactive shards - if err := verifyInactiveTrees(rekorClient, serverURL, logInfo.InactiveShards); err != nil { + if err := verifyInactiveTrees(ctx, rekorClient, serverURL, logInfo.InactiveShards); err != nil { return nil, err } @@ -97,7 +97,7 @@ var logInfoCmd = &cobra.Command{ } treeID := swag.StringValue(logInfo.TreeID) - if err := verifyTree(rekorClient, signedTreeHead, serverURL, treeID); err != nil { + if err := verifyTree(ctx, rekorClient, signedTreeHead, serverURL, treeID); err != nil { return nil, err } @@ -112,7 +112,7 @@ var logInfoCmd = &cobra.Command{ }), } -func verifyInactiveTrees(rekorClient *rclient.Rekor, serverURL string, inactiveShards []*models.InactiveShardLogInfo) error { +func verifyInactiveTrees(ctx context.Context, rekorClient *rclient.Rekor, serverURL string, inactiveShards []*models.InactiveShardLogInfo) error { if inactiveShards == nil { return nil } @@ -120,7 +120,7 @@ func verifyInactiveTrees(rekorClient *rclient.Rekor, serverURL string, inactiveS for _, shard := range inactiveShards { signedTreeHead := swag.StringValue(shard.SignedTreeHead) treeID := swag.StringValue(shard.TreeID) - if err := verifyTree(rekorClient, signedTreeHead, serverURL, treeID); err != nil { + if err := verifyTree(ctx, rekorClient, signedTreeHead, serverURL, treeID); err != nil { return fmt.Errorf("verifying inactive shard with ID %s: %w", treeID, err) } } @@ -128,7 +128,7 @@ func verifyInactiveTrees(rekorClient *rclient.Rekor, serverURL string, inactiveS return nil } -func verifyTree(rekorClient *rclient.Rekor, signedTreeHead, serverURL, treeID string) error { +func verifyTree(ctx context.Context, rekorClient *rclient.Rekor, signedTreeHead, serverURL, treeID string) error { oldState := state.Load(serverURL) if treeID != "" { oldState = state.Load(treeID) @@ -145,8 +145,12 @@ func verifyTree(rekorClient *rclient.Rekor, signedTreeHead, serverURL, treeID st return errors.New("signature on tree head did not verify") } - if err := proveConsistency(rekorClient, oldState, sth, treeID); err != nil { - return err + if oldState != nil { + if err := verify.ProveConsistency(ctx, rekorClient, oldState, &sth, treeID); err != nil { + return err + } + } else { + log.CliLogger.Infof("No previous log state stored, unable to prove consistency") } if viper.GetBool("store_tree_state") { @@ -162,45 +166,6 @@ func verifyTree(rekorClient *rclient.Rekor, signedTreeHead, serverURL, treeID st return nil } -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 - } - persistedSize := oldState.Size - switch { - case persistedSize < sth.Size: - log.CliLogger.Infof("Found previous log state, proving consistency between %d and %d", oldState.Size, sth.Size) - params := tlog.NewGetLogProofParams() - firstSize := int64(persistedSize) - params.FirstSize = &firstSize - params.LastSize = int64(sth.Size) - params.TreeID = &treeID - logProof, err := rekorClient.Tlog.GetLogProof(params) - if err != nil { - return err - } - hashes := [][]byte{} - for _, h := range logProof.Payload.Hashes { - b, _ := hex.DecodeString(h) - hashes = append(hashes, b) - } - if err := proof.VerifyConsistency(rfc6962.DefaultHasher, persistedSize, sth.Size, hashes, oldState.Hash, - sth.Hash); err != nil { - return err - } - log.CliLogger.Infof("Consistency proof valid!") - case persistedSize == sth.Size: - if !bytes.Equal(oldState.Hash, sth.Hash) { - return errors.New("root hash returned from server does not match previously persisted state") - } - log.CliLogger.Infof("Persisted log state matches the current state of the log") - default: - return fmt.Errorf("current size of tree reported from server %d is less than previously persisted state %d", sth.Size, persistedSize) - } - return nil -} - func loadVerifier(rekorClient *rclient.Rekor) (signature.Verifier, error) { publicKey := viper.GetString("rekor_server_public_key") if publicKey == "" { diff --git a/cmd/rekor-cli/app/upload.go b/cmd/rekor-cli/app/upload.go index 32739e409..f504072db 100644 --- a/cmd/rekor-cli/app/upload.go +++ b/cmd/rekor-cli/app/upload.go @@ -17,8 +17,6 @@ package app import ( "context" - "crypto/ecdsa" - "crypto/sha256" "fmt" "io" "net/http" @@ -26,7 +24,6 @@ import ( "os" "path/filepath" - "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/go-openapi/runtime" "github.com/go-openapi/swag" "github.com/spf13/cobra" @@ -34,12 +31,11 @@ import ( "github.com/sigstore/rekor/cmd/rekor-cli/app/format" "github.com/sigstore/rekor/pkg/client" - genclient "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/client/entries" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/types" - "github.com/sigstore/rekor/pkg/util" + "github.com/sigstore/rekor/pkg/verify" ) type uploadCmdOutput struct { @@ -138,7 +134,11 @@ var uploadCmd = &cobra.Command{ } // verify log entry - if verified, err := verifyLogEntry(ctx, rekorClient, logEntry); err != nil || !verified { + verifier, err := loadVerifier(rekorClient) + if err != nil { + return nil, fmt.Errorf("retrieving rekor public key") + } + if err := verify.VerifySignedEntryTimestamp(ctx, &logEntry, verifier); err != nil { return nil, fmt.Errorf("unable to verify entry was added to log: %w", err) } @@ -149,45 +149,6 @@ var uploadCmd = &cobra.Command{ }), } -func verifyLogEntry(ctx context.Context, rekorClient *genclient.Rekor, logEntry models.LogEntryAnon) (bool, error) { - if logEntry.Verification == nil { - return false, nil - } - // verify the entry - if logEntry.Verification.SignedEntryTimestamp == nil { - return false, fmt.Errorf("signature missing") - } - - le := &models.LogEntryAnon{ - IntegratedTime: logEntry.IntegratedTime, - LogIndex: logEntry.LogIndex, - Body: logEntry.Body, - LogID: logEntry.LogID, - } - - payload, err := le.MarshalBinary() - if err != nil { - return false, err - } - canonicalized, err := jsoncanonicalizer.Transform(payload) - if err != nil { - return false, err - } - - // get rekor's public key - rekorPubKey, err := util.PublicKey(ctx, rekorClient) - if err != nil { - return false, err - } - - // verify the SET against the public key - hash := sha256.Sum256(canonicalized) - if !ecdsa.VerifyASN1(rekorPubKey, hash[:], []byte(logEntry.Verification.SignedEntryTimestamp)) { - return false, fmt.Errorf("unable to verify") - } - return true, nil -} - func init() { initializePFlagMap() if err := addArtifactPFlags(uploadCmd); err != nil { diff --git a/cmd/rekor-cli/app/verify.go b/cmd/rekor-cli/app/verify.go index bf80b31a3..85293174a 100644 --- a/cmd/rekor-cli/app/verify.go +++ b/cmd/rekor-cli/app/verify.go @@ -16,9 +16,7 @@ package app import ( - "bytes" "context" - "encoding/base64" "encoding/hex" "fmt" "math/bits" @@ -26,7 +24,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/transparency-dev/merkle/proof" "github.com/transparency-dev/merkle/rfc6962" "github.com/sigstore/rekor/cmd/rekor-cli/app/format" @@ -36,6 +33,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/verify" ) type verifyCmdOutput struct { @@ -88,6 +86,7 @@ var verifyCmd = &cobra.Command{ return nil }, Run: format.WrapCmd(func(args []string) (interface{}, error) { + ctx := context.Background() rekorClient, err := client.GetRekorClient(viper.GetString("rekor_server"), client.WithUserAgent(UserAgent())) if err != nil { return nil, err @@ -139,7 +138,7 @@ var verifyCmd = &cobra.Command{ logEntry := resp.Payload[0] var o *verifyCmdOutput - var entryBytes []byte + var entry models.LogEntryAnon for k, v := range logEntry { o = &verifyCmdOutput{ RootHash: *v.Verification.InclusionProof.RootHash, @@ -148,10 +147,7 @@ var verifyCmd = &cobra.Command{ Size: *v.Verification.InclusionProof.TreeSize, Hashes: v.Verification.InclusionProof.Hashes, } - entryBytes, err = base64.StdEncoding.DecodeString(v.Body.(string)) - if err != nil { - return nil, err - } + entry = v } if viper.IsSet("uuid") { @@ -164,23 +160,17 @@ var verifyCmd = &cobra.Command{ } } - // Note: the returned entry UUID is the UUID (not include the Tree ID) - leafHash, _ := hex.DecodeString(o.EntryUUID) - if !bytes.Equal(rfc6962.DefaultHasher.HashLeaf(entryBytes), leafHash) { - return nil, fmt.Errorf("computed leaf hash did not match entry UUID") + // Get Rekor Pub + // TODO(asraa): Replace with sigstore's GetRekorPubs to use TUF. + verifier, err := loadVerifier(rekorClient) + if err != nil { + return nil, err } - hashes := [][]byte{} - for _, h := range o.Hashes { - hb, _ := hex.DecodeString(h) - hashes = append(hashes, hb) + if err := verify.VerifyLogEntry(ctx, &entry, verifier); err != nil { + return nil, fmt.Errorf("validating entry: %w", err) } - rootHash, _ := hex.DecodeString(o.RootHash) - - if err := proof.VerifyInclusion(rfc6962.DefaultHasher, uint64(o.Index), uint64(o.Size), leafHash, hashes, rootHash); err != nil { - return nil, err - } return o, err }), } diff --git a/pkg/verify/verify.go b/pkg/verify/verify.go new file mode 100644 index 000000000..516aed339 --- /dev/null +++ b/pkg/verify/verify.go @@ -0,0 +1,207 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// 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 verify + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/client/tlog" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/util" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" + "github.com/transparency-dev/merkle/proof" + "github.com/transparency-dev/merkle/rfc6962" +) + +// ProveConsistency verifies consistency between an initial, trusted STH +// and a second new STH. Callers MUST verify signature on the STHs'. +func ProveConsistency(ctx context.Context, rClient *client.Rekor, + oldSTH *util.SignedCheckpoint, newSTH *util.SignedCheckpoint, treeID string) error { + oldTreeSize := int64(oldSTH.Size) + switch { + case oldTreeSize == int64(newSTH.Size): + if !bytes.Equal(oldSTH.Hash, newSTH.Hash) { + return errors.New("old root hash does not match STH hash") + } + case oldTreeSize < int64(newSTH.Size): + consistencyParams := tlog.NewGetLogProofParamsWithContext(ctx) + consistencyParams.FirstSize = &oldTreeSize // Root size at the old, or trusted state. + consistencyParams.LastSize = int64(newSTH.Size) // Root size at the new state to verify against. + consistencyParams.TreeID = &treeID + consistencyProof, err := rClient.Tlog.GetLogProof(consistencyParams) + if err != nil { + return err + } + var hashes [][]byte + for _, h := range consistencyProof.Payload.Hashes { + b, err := hex.DecodeString(h) + if err != nil { + return errors.New("error decoding consistency proof hashes") + } + hashes = append(hashes, b) + } + if err := proof.VerifyConsistency(rfc6962.DefaultHasher, + oldSTH.Size, newSTH.Size, hashes, oldSTH.Hash, newSTH.Hash); err != nil { + return err + } + case oldTreeSize > int64(newSTH.Size): + return errors.New("inclusion proof returned a tree size larger than the verified tree size") + } + return nil + +} + +// VerifyCurrentCheckpoint verifies the provided checkpoint by verifying consistency +// against a newly fetched Checkpoint. +//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. + if !oldSTH.Verify(verifier) { + return nil, errors.New("signature on old tree head did not verify") + } + + // Get and verify against the current STH. + infoParams := tlog.NewGetLogInfoParamsWithContext(ctx) + result, err := rClient.Tlog.GetLogInfo(infoParams) + if err != nil { + return nil, err + } + + logInfo := result.GetPayload() + sth := util.SignedCheckpoint{} + if err := sth.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil { + return nil, err + } + + // Verify the signature on the SignedCheckpoint. + if !sth.Verify(verifier) { + return nil, errors.New("signature on tree head did not verify") + } + + // Now verify consistency up to the STH. + if err := ProveConsistency(ctx, rClient, oldSTH, &sth, *logInfo.TreeID); err != nil { + return nil, err + } + return &sth, 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 +func VerifyInclusion(ctx context.Context, e *models.LogEntryAnon) error { + if e.Verification == nil || e.Verification.InclusionProof == nil { + return errors.New("inclusion proof not provided") + } + + hashes := [][]byte{} + for _, h := range e.Verification.InclusionProof.Hashes { + hb, _ := hex.DecodeString(h) + hashes = append(hashes, hb) + } + + rootHash, err := hex.DecodeString(*e.Verification.InclusionProof.RootHash) + if err != nil { + return err + } + + // Verify the inclusion proof. + entryBytes, err := base64.StdEncoding.DecodeString(e.Body.(string)) + if err != nil { + return err + } + leafHash := rfc6962.DefaultHasher.HashLeaf(entryBytes) + + if err := proof.VerifyInclusion(rfc6962.DefaultHasher, uint64(*e.Verification.InclusionProof.LogIndex), + uint64(*e.Verification.InclusionProof.TreeSize), leafHash, hashes, rootHash); err != nil { + return err + } + + return nil +} + +// VerifySignedEntryTimestamp verifies the entry's SET against the provided +// public key. +//nolint +func VerifySignedEntryTimestamp(ctx context.Context, e *models.LogEntryAnon, verifier signature.Verifier) error { + if e.Verification == nil { + return fmt.Errorf("missing verification") + } + if e.Verification.SignedEntryTimestamp == nil { + return fmt.Errorf("signature missing") + } + + type bundle struct { + Body interface{} `json:"body"` + IntegratedTime int64 `json:"integratedTime"` + // Note that this is the virtual index. + LogIndex int64 `json:"logIndex"` + LogID string `json:"logID"` + } + bundlePayload := bundle{ + Body: e.Body, + IntegratedTime: *e.IntegratedTime, + LogIndex: *e.LogIndex, + LogID: *e.LogID, + } + contents, err := json.Marshal(bundlePayload) + if err != nil { + return fmt.Errorf("marshaling bundle: %w", err) + } + canonicalized, err := jsoncanonicalizer.Transform(contents) + if err != nil { + return fmt.Errorf("canonicalizing bundle: %w", err) + } + + // verify the SET against the public key + if err := verifier.VerifySignature(bytes.NewReader(e.Verification.SignedEntryTimestamp), + bytes.NewReader(canonicalized), options.WithContext(ctx)); err != nil { + return fmt.Errorf("unable to verify bundle: %w", err) + } + return nil +} + +// VerifyLogEntry performs verification of a LogEntry given a Rekor verifier. +// Performs inclusion proof verification up to a verified root hash and +// SignedEntryTimestamp verification. +//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 { + return err + } + + // TODO: If/when we return an STH in the response, verify that too, against an + // optional known STH as well. + // See https://github.com/sigstore/rekor/issues/988 + + // Verify the Signed Entry Timestamp. + if err := VerifySignedEntryTimestamp(ctx, e, verifier); err != nil { + return err + } + + return nil +} diff --git a/pkg/verify/verify_test.go b/pkg/verify/verify_test.go new file mode 100644 index 000000000..41cbea77f --- /dev/null +++ b/pkg/verify/verify_test.go @@ -0,0 +1,225 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// 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 verify + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/client/tlog" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/util" +) + +type TlogClient struct { + Proof []string + Root string + LogInfo models.LogInfo +} + +func (m *TlogClient) GetLogProof(params *tlog.GetLogProofParams, opts ...tlog.ClientOption) (*tlog.GetLogProofOK, error) { + return &tlog.GetLogProofOK{ + Payload: &models.ConsistencyProof{ + Hashes: m.Proof, + RootHash: &m.Root, + }}, nil +} + +func (m *TlogClient) GetLogInfo(params *tlog.GetLogInfoParams, opts ...tlog.ClientOption) (*tlog.GetLogInfoOK, error) { + return &tlog.GetLogInfoOK{ + Payload: &m.LogInfo, + }, nil +} + +// TODO: Implement mock +func (m *TlogClient) SetTransport(transport runtime.ClientTransport) { +} + +func TestConsistency(t *testing.T) { + root2String := "5be1758dd2228acfaf2546b4b6ce8aa40c82a3748f3dcb550e0d67ba34f02a45" + root2, _ := hex.DecodeString(root2String) + root1, _ := hex.DecodeString("59a575f157274702c38de3ab1e1784226f391fb79500ebf9f02b4439fb77574c") + hashes := []string{"d3be742c8d73e2dd3c5635843e987ad3dfb3837616f412a07bf730c3ad73f5cb"} + for _, test := range []struct { + name string + oldC util.Checkpoint + newC util.Checkpoint + Proof []string + wantErr bool + }{ + { + name: "zero length proof", + oldC: util.Checkpoint{ + Origin: "test", + Size: uint64(2), + Hash: root2, + }, + newC: util.Checkpoint{ + Origin: "test", + Size: uint64(2), + Hash: root2, + }, + wantErr: false, + }, + { + name: "valid consistency proof", + oldC: util.Checkpoint{ + Origin: "test", + Size: uint64(1), + Hash: root1, + }, + newC: util.Checkpoint{ + Origin: "test", + Size: uint64(2), + Hash: root2, + }, + wantErr: false, + }, + { + name: "invalid new sth request", + oldC: util.Checkpoint{ + Origin: "test", + Size: uint64(2), + Hash: root1, + }, + newC: util.Checkpoint{ + Origin: "test", + Size: uint64(1), + Hash: root2, + }, + wantErr: true, + }, + { + name: "invalid consistency proof", + oldC: util.Checkpoint{ + Origin: "test", + Size: uint64(1), + Hash: root2, + }, + newC: util.Checkpoint{ + Origin: "test", + Size: uint64(2), + Hash: root1, + }, + wantErr: true, + }, + { + name: "invalid consistency - same size", + oldC: util.Checkpoint{ + Origin: "test", + Size: uint64(1), + Hash: root1, + }, + newC: util.Checkpoint{ + Origin: "test", + Size: uint64(1), + Hash: root2, + }, + wantErr: true, + }, + } { + var mClient client.Rekor + mClient.Tlog = &TlogClient{Proof: hashes, Root: root2String} + + t.Run(string(test.name), func(t *testing.T) { + + ctx := context.Background() + treeID := "123" + oldSTH, err := util.CreateSignedCheckpoint(test.oldC) + if err != nil { + t.Fatalf("creating old checkpoint") + } + newSTH, err := util.CreateSignedCheckpoint(test.newC) + if err != nil { + t.Fatalf("creating new checkpoint") + } + + gotErr := ProveConsistency(ctx, &mClient, oldSTH, newSTH, treeID) + + if (gotErr != nil) != test.wantErr { + t.Fatalf("ProveConsistency = %t, wantErr %t", gotErr, test.wantErr) + } + }) + } +} + +func TestInclusion(t *testing.T) { + time := int64(1661794812) + logID := "1701474e8cb504dbb853a5887bc2cf66936b0f36d2641bfb61f1abae80088e6a" + for _, test := range []struct { + name string + e models.LogEntryAnon + wantErr bool + }{ + { + name: "valid inclusion", + e: models.LogEntryAnon{ + Body: "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJlY2RjNTUzNmY3M2JkYWU4ODE2ZjBlYTQwNzI2ZWY1ZTliODEwZDkxNDQ5MzA3NTkwM2JiOTA2MjNkOTdiMWQ4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUQvUGRQUW1LV0MxKzBCTkVkNWdLdlFHcjF4eGwzaWVVZmZ2M2prMXp6Skt3SWhBTEJqM3hmQXlXeGx6NGpwb0lFSVYxVWZLOXZua1VVT1NvZVp4QlpQSEtQQyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRVOWpWR1pTUWxNNWFtbFlUVGd4UmxvNFoyMHZNU3R2YldWTmR3cHRiaTh6TkRjdk5UVTJaeTlzY21sVE56SjFUV2haT1V4alZDczFWVW8yWmtkQ1oyeHlOVm80VERCS1RsTjFZWE41WldRNVQzUmhVblozUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19", + IntegratedTime: &time, + LogID: &logID, + LogIndex: swag.Int64(1), + Verification: &models.LogEntryAnonVerification{ + InclusionProof: &models.InclusionProof{ + TreeSize: swag.Int64(int64(2)), + RootHash: swag.String("5be1758dd2228acfaf2546b4b6ce8aa40c82a3748f3dcb550e0d67ba34f02a45"), + LogIndex: swag.Int64(1), + Hashes: []string{ + "59a575f157274702c38de3ab1e1784226f391fb79500ebf9f02b4439fb77574c", + }, + }, + SignedEntryTimestamp: strfmt.Base64("MEUCIHJj8xP+oPTd4BAXhO2lcbRplnKW2FafMiFo0gIDGUcYAiEA80BJ8QikiupGAv3R3dtSvZ1ICsAOQat10cFKPqBkLBM="), + }, + }, + wantErr: false, + }, + { + name: "invalid inclusion - bad body hash", + e: models.LogEntryAnon{ + Body: "ayJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJlY2RjNTUzNmY3M2JkYWU4ODE2ZjBlYTQwNzI2ZWY1ZTliODEwZDkxNDQ5MzA3NTkwM2JiOTA2MjNkOTdiMWQ4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUQvUGRQUW1LV0MxKzBCTkVkNWdLdlFHcjF4eGwzaWVVZmZ2M2prMXp6Skt3SWhBTEJqM3hmQXlXeGx6NGpwb0lFSVYxVWZLOXZua1VVT1NvZVp4QlpQSEtQQyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRVOWpWR1pTUWxNNWFtbFlUVGd4UmxvNFoyMHZNU3R2YldWTmR3cHRiaTh6TkRjdk5UVTJaeTlzY21sVE56SjFUV2haT1V4alZDczFWVW8yWmtkQ1oyeHlOVm80VERCS1RsTjFZWE41WldRNVQzUmhVblozUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19", + IntegratedTime: &time, + LogID: &logID, + LogIndex: swag.Int64(1), + Verification: &models.LogEntryAnonVerification{ + InclusionProof: &models.InclusionProof{ + TreeSize: swag.Int64(int64(2)), + RootHash: swag.String("5be1758dd2228acfaf2546b4b6ce8aa40c82a3748f3dcb550e0d67ba34f02a45"), + LogIndex: swag.Int64(1), + Hashes: []string{ + "59a575f157274702c38de3ab1e1784226f391fb79500ebf9f02b4439fb77574c", + }, + }, + SignedEntryTimestamp: strfmt.Base64("MEUCIHJj8xP+oPTd4BAXhO2lcbRplnKW2FafMiFo0gIDGUcYAiEA80BJ8QikiupGAv3R3dtSvZ1ICsAOQat10cFKPqBkLBM="), + }, + }, + wantErr: true, + }, + } { + t.Run(string(test.name), func(t *testing.T) { + ctx := context.Background() + + gotErr := VerifyInclusion(ctx, &test.e) + + if (gotErr != nil) != test.wantErr { + t.Fatalf("VerifyInclusion = %t, wantErr %t", gotErr, test.wantErr) + } + }) + } +}