From 970fc7f9bddcba46664755c3d4a25282723703dd Mon Sep 17 00:00:00 2001 From: Asra Ali Date: Thu, 18 Aug 2022 12:40:31 -0500 Subject: [PATCH] feat: add verification functions Signed-off-by: Asra Ali lint and comment Signed-off-by: Asra Ali update Signed-off-by: Asra Ali --- cmd/rekor-cli/app/log_info.go | 66 +++--------- cmd/rekor-cli/app/verify.go | 32 ++---- pkg/verify/verify.go | 196 ++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 71 deletions(-) create mode 100644 pkg/verify/verify.go diff --git a/cmd/rekor-cli/app/log_info.go b/cmd/rekor-cli/app/log_info.go index 78ce185f8..371f34c4d 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,13 @@ 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.Hash, + int64(oldState.Size), 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 +167,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/verify.go b/cmd/rekor-cli/app/verify.go index bf80b31a3..b6aab6d81 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, rekorClient, &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..c00c74701 --- /dev/null +++ b/pkg/verify/verify.go @@ -0,0 +1,196 @@ +// +// 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" +) + +func ProveConsistency(ctx context.Context, rClient *client.Rekor, + oldStateHash []byte, oldTreeSize int64, sth util.SignedCheckpoint, treeID string) error { + switch { + case oldTreeSize == int64(sth.Size): + if !bytes.Equal(oldStateHash, sth.Hash) { + return errors.New("old root hash does not match STH hash") + } + case oldTreeSize < int64(sth.Size): + consistencyParams := tlog.NewGetLogProofParamsWithContext(ctx) + consistencyParams.FirstSize = &oldTreeSize // Root hash at the time the proof was returned + consistencyParams.LastSize = int64(sth.Size) // Root hash verified with rekor pubkey + 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, + uint64(oldTreeSize), sth.Size, hashes, oldStateHash, sth.Hash); err != nil { + return err + } + case oldTreeSize > int64(sth.Size): + return errors.New("inclusion proof returned a tree size larger than the verified tree size") + } + return nil + +} + +// VerifyRootHash verifies the provided root hash at the treeSize by verifying consistenct +// against an STH. +//nolint +func VerifyRootHash(ctx context.Context, rClient *client.Rekor, verifier signature.Verifier, + rootHash []byte, treeSize int64, treeID string) error { + // Get and verify the current STH. + infoParams := tlog.NewGetLogInfoParamsWithContext(ctx) + result, err := rClient.Tlog.GetLogInfo(infoParams) + if err != nil { + return err + } + + logInfo := result.GetPayload() + + sth := util.SignedCheckpoint{} + if err := sth.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil { + return err + } + + if !sth.Verify(verifier) { + return errors.New("signature on tree head did not verify") + } + + // Now verify consistency up to the STH. + return ProveConsistency(ctx, rClient, rootHash, treeSize, sth, treeID) +} + +// VerifyInclusion verifies an entry's inclusion proof. Clients MUST also +// verify the root hash via VerifyRootHash. +//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 { + 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, rClient *client.Rekor, e *models.LogEntryAnon, verifier signature.Verifier) error { + // Verify the inclusion proof. + if err := VerifyInclusion(ctx, e); err != nil { + return err + } + + // Verify the root hash. + rootHash, err := hex.DecodeString(*e.Verification.InclusionProof.RootHash) + if err != nil { + return err + } + tid := fmt.Sprintf("%x", *e.LogID) + + if err := VerifyRootHash(ctx, rClient, verifier, + rootHash, *e.Verification.InclusionProof.TreeSize, tid); err != nil { + return err + } + + // Verify the Signed Entry Timestamp. + if err := VerifySignedEntryTimestamp(ctx, e, verifier); err != nil { + return err + } + + return nil +}