From a8f9450c219be4ab70a5b4e9d45ef4905836e0b0 Mon Sep 17 00:00:00 2001 From: Bob Callaway <bobcallaway@users.noreply.github.com> Date: Mon, 7 Jun 2021 15:40:03 -0400 Subject: [PATCH] Convert STH to checkpoint format (#322) * Convert STH to checkpoint format This switches away from sending the (now deprecated) Trillian LogRootV1 format over to the checkpoint format documented at https://github.com/google/trillian-examples/tree/master/formats/log Fixes: #313 Signed-off-by: Bob Callaway <bob.callaway@gmail.com> --- cmd/rekor-cli/app/log_info.go | 52 +- cmd/rekor-cli/app/root.go | 5 +- cmd/rekor-cli/app/state/state.go | 10 +- cmd/rekor-server/app/root.go | 1 + cmd/rekor-server/app/watch.go | 33 +- go.mod | 3 +- go.sum | 3 +- openapi.yaml | 20 +- pkg/api/error.go | 1 + pkg/api/tlog.go | 40 +- pkg/generated/models/log_info.go | 131 +----- .../restapi/configure_rekor_server.go | 2 + pkg/generated/restapi/embedded_spec.go | 76 +-- pkg/util/checkpoint.go | 319 +++++++++++++ pkg/util/checkpoint_test.go | 443 ++++++++++++++++++ pkg/verify/log_root.go | 65 --- pkg/verify/log_root_test.go | 55 --- 17 files changed, 851 insertions(+), 408 deletions(-) create mode 100644 pkg/util/checkpoint.go create mode 100644 pkg/util/checkpoint_test.go delete mode 100644 pkg/verify/log_root.go delete mode 100644 pkg/verify/log_root_test.go diff --git a/cmd/rekor-cli/app/log_info.go b/cmd/rekor-cli/app/log_info.go index 95e781f..e52f509 100644 --- a/cmd/rekor-cli/app/log_info.go +++ b/cmd/rekor-cli/app/log_info.go @@ -22,7 +22,6 @@ import ( "encoding/pem" "errors" "fmt" - "strings" "time" "github.com/google/trillian/merkle/logverifier" @@ -34,7 +33,7 @@ import ( "github.com/sigstore/rekor/cmd/rekor-cli/app/state" "github.com/sigstore/rekor/pkg/generated/client/tlog" "github.com/sigstore/rekor/pkg/log" - "github.com/sigstore/rekor/pkg/verify" + "github.com/sigstore/rekor/pkg/util" ) type logInfoCmdOutput struct { @@ -72,14 +71,11 @@ var logInfoCmd = &cobra.Command{ logInfo := result.GetPayload() - logRoot := *logInfo.SignedTreeHead.LogRoot - if logRoot == nil { - return nil, errors.New("logroot should not be nil") - } - signature := *logInfo.SignedTreeHead.Signature - if signature == nil { - return nil, errors.New("signature should not be nil") + sth := util.RekorSTH{} + if err := sth.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil { + return nil, err } + publicKey := viper.GetString("rekor_server_public_key") if publicKey == "" { // fetch key from server @@ -100,33 +96,25 @@ var logInfoCmd = &cobra.Command{ return nil, err } - lr, err := verify.SignedLogRoot(pub, logRoot, signature) - if err != nil { - return nil, err + if !sth.Verify(pub) { + return nil, errors.New("signature on tree head did not verify") } + cmdOutput := &logInfoCmdOutput{ TreeSize: *logInfo.TreeSize, RootHash: *logInfo.RootHash, - TimestampNanos: lr.TimestampNanos, - } - - if lr.TreeSize != uint64(*logInfo.TreeSize) { - return nil, errors.New("tree size in signed tree head does not match value returned in API call") - } - - if !strings.EqualFold(hex.EncodeToString(lr.RootHash), *logInfo.RootHash) { - return nil, errors.New("root hash in signed tree head does not match value returned in API call") + TimestampNanos: sth.GetTimestamp(), } oldState := state.Load(serverURL) if oldState != nil { - persistedSize := oldState.TreeSize - if persistedSize < lr.TreeSize { - log.CliLogger.Infof("Found previous log state, proving consistency between %d and %d", oldState.TreeSize, lr.TreeSize) + persistedSize := oldState.Size + if 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(lr.TreeSize) + params.LastSize = int64(sth.Size) proof, err := rekorClient.Tlog.GetLogProof(params) if err != nil { return nil, err @@ -137,25 +125,25 @@ var logInfoCmd = &cobra.Command{ hashes = append(hashes, b) } v := logverifier.New(rfc6962.DefaultHasher) - if err := v.VerifyConsistencyProof(firstSize, int64(lr.TreeSize), oldState.RootHash, - lr.RootHash, hashes); err != nil { + if err := v.VerifyConsistencyProof(firstSize, int64(sth.Size), oldState.Hash, + sth.Hash, hashes); err != nil { return nil, err } log.CliLogger.Infof("Consistency proof valid!") - } else if persistedSize == lr.TreeSize { - if !bytes.Equal(oldState.RootHash, lr.RootHash) { + } else if persistedSize == sth.Size { + if !bytes.Equal(oldState.Hash, sth.Hash) { return nil, 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") - } else if persistedSize > lr.TreeSize { - return nil, fmt.Errorf("current size of tree reported from server %d is less than previously persisted state %d", lr.TreeSize, persistedSize) + } else if persistedSize > sth.Size { + return nil, fmt.Errorf("current size of tree reported from server %d is less than previously persisted state %d", sth.Size, persistedSize) } } else { log.CliLogger.Infof("No previous log state stored, unable to prove consistency") } if viper.GetBool("store_tree_state") { - if err := state.Dump(serverURL, lr); err != nil { + if err := state.Dump(serverURL, &sth); err != nil { log.CliLogger.Infof("Unable to store previous state: %v", err) } } diff --git a/cmd/rekor-cli/app/root.go b/cmd/rekor-cli/app/root.go index c9fae09..16b349b 100644 --- a/cmd/rekor-cli/app/root.go +++ b/cmd/rekor-cli/app/root.go @@ -131,7 +131,10 @@ func GetRekorClient(rekorServerURL string) (*client.Rekor, error) { if viper.GetString("api-key") != "" { rt.DefaultAuthentication = httptransport.APIKeyAuth("apiKey", "query", viper.GetString("api-key")) } - return client.New(rt, strfmt.Default), nil + + registry := strfmt.Default + registry.Add("signedCheckpoint", &util.SignedCheckpoint{}, util.SignedCheckpointValidator) + return client.New(rt, registry), nil } type urlFlag struct { diff --git a/cmd/rekor-cli/app/state/state.go b/cmd/rekor-cli/app/state/state.go index bcc3989..9ae0807 100644 --- a/cmd/rekor-cli/app/state/state.go +++ b/cmd/rekor-cli/app/state/state.go @@ -21,13 +21,13 @@ import ( "os" "path/filepath" - "github.com/google/trillian/types" "github.com/mitchellh/go-homedir" + "github.com/sigstore/rekor/pkg/util" ) -type persistedState map[string]*types.LogRootV1 +type persistedState map[string]*util.RekorSTH -func Dump(url string, lr *types.LogRootV1) error { +func Dump(url string, sth *util.RekorSTH) error { rekorDir, err := getRekorDir() if err != nil { return err @@ -38,7 +38,7 @@ func Dump(url string, lr *types.LogRootV1) error { if state == nil { state = make(persistedState) } - state[url] = lr + state[url] = sth b, err := json.Marshal(&state) if err != nil { @@ -67,7 +67,7 @@ func loadStateFile() persistedState { return result } -func Load(url string) *types.LogRootV1 { +func Load(url string) *util.RekorSTH { if state := loadStateFile(); state != nil { return state[url] } diff --git a/cmd/rekor-server/app/root.go b/cmd/rekor-server/app/root.go index 0b11aba..33022ae 100644 --- a/cmd/rekor-server/app/root.go +++ b/cmd/rekor-server/app/root.go @@ -59,6 +59,7 @@ func init() { rootCmd.PersistentFlags().String("trillian_log_server.address", "127.0.0.1", "Trillian log server address") rootCmd.PersistentFlags().Uint16("trillian_log_server.port", 8090, "Trillian log server port") rootCmd.PersistentFlags().Uint("trillian_log_server.tlog_id", 0, "Trillian tree id") + rootCmd.PersistentFlags().String("rekor_server.hostname", "rekor.sigstore.dev", "public hostname of instance") rootCmd.PersistentFlags().String("rekor_server.address", "127.0.0.1", "Address to bind to") rootCmd.PersistentFlags().String("rekor_server.signer", "memory", "Rekor signer to use. Current valid options include: [gcpkms, memory]") rootCmd.PersistentFlags().String("rekor_server.timestamp_chain", "", "PEM encoded cert chain to use for timestamping") diff --git a/cmd/rekor-server/app/watch.go b/cmd/rekor-server/app/watch.go index 5e2d043..1278cbd 100644 --- a/cmd/rekor-server/app/watch.go +++ b/cmd/rekor-server/app/watch.go @@ -29,7 +29,6 @@ import ( _ "gocloud.dev/blob/fileblob" // fileblob _ "gocloud.dev/blob/gcsblob" - "github.com/google/trillian/types" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -37,9 +36,8 @@ import ( "github.com/sigstore/rekor/cmd/rekor-cli/app" "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/log" - "github.com/sigstore/rekor/pkg/verify" + "github.com/sigstore/rekor/pkg/util" ) const rekorSthBucketEnv = "REKOR_STH_BUCKET" @@ -109,10 +107,10 @@ var watchCmd = &cobra.Command{ log.Logger.Warnf("error verifiying tree: %s", err) continue } - log.Logger.Infof("Found and verified state at %d %d", lr.VerifiedLogRoot.TreeSize, lr.VerifiedLogRoot.TimestampNanos) - if last != nil && last.VerifiedLogRoot.TreeSize == lr.VerifiedLogRoot.TreeSize { + log.Logger.Infof("Found and verified state at %d", lr.VerifiedLogRoot.Size) + if last != nil && last.VerifiedLogRoot.Size == lr.VerifiedLogRoot.Size { log.Logger.Infof("Last tree size is the same as the current one: %d %d", - last.VerifiedLogRoot.TreeSize, lr.VerifiedLogRoot.TreeSize) + last.VerifiedLogRoot.Size, lr.VerifiedLogRoot.Size) // If it's the same, it shouldn't have changed but we'll still upload anyway // in case that failed. } @@ -136,23 +134,17 @@ func doCheck(c *client.Rekor, pub crypto.PublicKey) (*SignedAndUnsignedLogRoot, if err != nil { return nil, errors.Wrap(err, "getting log info") } - logRoot := *li.Payload.SignedTreeHead.LogRoot - if logRoot == nil { - return nil, errors.New("logroot should not be nil") - } - signature := *li.Payload.SignedTreeHead.Signature - if signature == nil { - return nil, errors.New("signature should not be nil") + sth := util.RekorSTH{} + if err := sth.UnmarshalText([]byte(*li.Payload.SignedTreeHead)); err != nil { + return nil, errors.Wrap(err, "unmarshalling tree head") } - verifiedLogRoot, err := verify.SignedLogRoot(pub, logRoot, signature) - if err != nil { - return nil, errors.Wrap(err, "signing log root") + if !sth.Verify(pub) { + return nil, errors.Wrap(err, "signed tree head failed verification") } return &SignedAndUnsignedLogRoot{ - SignedLogRoot: li.GetPayload().SignedTreeHead, - VerifiedLogRoot: verifiedLogRoot, + VerifiedLogRoot: &sth, }, nil } @@ -162,7 +154,7 @@ func uploadToBlobStorage(ctx context.Context, bucket *blob.Bucket, lr *SignedAnd return err } - objName := fmt.Sprintf("sth-%d.json", lr.VerifiedLogRoot.TreeSize) + objName := fmt.Sprintf("sth-%d.json", lr.VerifiedLogRoot.Size) w, err := bucket.NewWriter(ctx, objName, nil) if err != nil { return err @@ -176,6 +168,5 @@ func uploadToBlobStorage(ctx context.Context, bucket *blob.Bucket, lr *SignedAnd // For JSON marshalling type SignedAndUnsignedLogRoot struct { - SignedLogRoot *models.LogInfoSignedTreeHead - VerifiedLogRoot *types.LogRootV1 + VerifiedLogRoot *util.RekorSTH } diff --git a/go.mod b/go.mod index 2580ef8..8584a86 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/go-openapi/swag v0.19.15 github.com/go-openapi/validate v0.20.2 github.com/go-playground/validator v9.31.0+incompatible - github.com/google/go-cmp v0.5.5 + github.com/google/go-cmp v0.5.6 github.com/google/rpmpack v0.0.0-20210107155803-d6befbf05148 github.com/google/trillian v1.3.14-0.20210413093047-5e12fb368c8f github.com/in-toto/in-toto-golang v0.1.1-0.20210528150343-f7dc21abaccf @@ -40,6 +40,7 @@ require ( go.uber.org/zap v1.17.0 gocloud.dev v0.23.0 golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf + golang.org/x/mod v0.4.2 golang.org/x/net v0.0.0-20210505214959-0714010a04ed golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect diff --git a/go.sum b/go.sum index f113f74..9d5f416 100644 --- a/go.sum +++ b/go.sum @@ -568,8 +568,9 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-containerregistry v0.4.1 h1:Lrcj2AOoZ7WKawsoKAh2O0dH0tBqMW2lTEmozmK4Z3k= github.com/google/go-containerregistry v0.4.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= diff --git a/openapi.yaml b/openapi.yaml index 2827d95..b4709ad 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -444,25 +444,9 @@ definitions: description: The current number of nodes in the merkle tree minimum: 1 signedTreeHead: - type: object + type: string + format: signedCheckpoint description: The current signed tree head - properties: - keyHint: - type: string - description: Key hint - format: byte - logRoot: - type: string - description: Log root - format: byte - signature: - type: string - description: Signature for log root - format: byte - required: - - keyHint - - logRoot - - signature required: - rootHash - treeSize diff --git a/pkg/api/error.go b/pkg/api/error.go index bb87015..9fd0543 100644 --- a/pkg/api/error.go +++ b/pkg/api/error.go @@ -45,6 +45,7 @@ const ( lastSizeGreaterThanKnown = "The tree size requested(%d) was greater than what is currently observable(%d)" signingError = "Error signing" failedToGenerateTimestampResponse = "Error generating timestamp response" + sthGenerateError = "Error generating signed tree head" ) func errorMsg(message string, code int) *models.Error { diff --git a/pkg/api/tlog.go b/pkg/api/tlog.go index 5a55ab5..7080a1f 100644 --- a/pkg/api/tlog.go +++ b/pkg/api/tlog.go @@ -16,17 +16,22 @@ package api import ( + "encoding/base64" + "encoding/binary" "encoding/hex" "fmt" "net/http" + "time" "github.com/go-openapi/runtime/middleware" - "github.com/go-openapi/strfmt" "github.com/google/trillian/types" + "github.com/spf13/viper" + "golang.org/x/mod/sumdb/note" "google.golang.org/grpc/codes" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/generated/restapi/operations/tlog" + "github.com/sigstore/rekor/pkg/util" ) // GetLogInfoHandler returns the current size of the tree and the STH @@ -46,25 +51,42 @@ func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder { hashString := hex.EncodeToString(root.RootHash) treeSize := int64(root.TreeSize) - logRoot := strfmt.Base64(result.SignedLogRoot.GetLogRoot()) + + sth := util.RekorSTH{ + SignedCheckpoint: util.SignedCheckpoint{ + Checkpoint: util.Checkpoint{ + Ecosystem: "Rekor", + Size: root.TreeSize, + Hash: root.RootHash, + }, + }, + } + sth.SetTimestamp(uint64(time.Now().UnixNano())) + // TODO: once api.signer implements crypto.Signer, switch to using Sign() API on Checkpoint // sign the log root ourselves to get the log root signature - sig, _, err := api.signer.Sign(params.HTTPRequest.Context(), result.SignedLogRoot.GetLogRoot()) + cpString, _ := sth.Checkpoint.MarshalText() + sig, _, err := api.signer.Sign(params.HTTPRequest.Context(), []byte(cpString)) if err != nil { - return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("signing error: %w", err), trillianCommunicationError) + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("signing error: %w", err), signingError) } - signature := strfmt.Base64(sig) + sth.Signatures = append(sth.Signatures, note.Signature{ + Name: viper.GetString("rekor_server.hostname"), + Hash: binary.BigEndian.Uint32([]byte(api.pubkeyHash)[0:4]), + Base64: base64.StdEncoding.EncodeToString(sig), + }) - sth := models.LogInfoSignedTreeHead{ - LogRoot: &logRoot, - Signature: &signature, + scBytes, err := sth.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: &sth, + SignedTreeHead: &scString, } return tlog.NewGetLogInfoOK().WithPayload(&logInfo) } diff --git a/pkg/generated/models/log_info.go b/pkg/generated/models/log_info.go index f51a1d4..b576bf0 100644 --- a/pkg/generated/models/log_info.go +++ b/pkg/generated/models/log_info.go @@ -40,9 +40,9 @@ type LogInfo struct { // Pattern: ^[0-9a-fA-F]{64}$ RootHash *string `json:"rootHash"` - // signed tree head + // The current signed tree head // Required: true - SignedTreeHead *LogInfoSignedTreeHead `json:"signedTreeHead"` + SignedTreeHead *string `json:"signedTreeHead"` // The current number of nodes in the merkle tree // Required: true @@ -91,15 +91,6 @@ func (m *LogInfo) validateSignedTreeHead(formats strfmt.Registry) error { return err } - if m.SignedTreeHead != nil { - if err := m.SignedTreeHead.Validate(formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("signedTreeHead") - } - return err - } - } - return nil } @@ -116,31 +107,8 @@ func (m *LogInfo) validateTreeSize(formats strfmt.Registry) error { return nil } -// ContextValidate validate this log info based on the context it is used +// ContextValidate validates this log info based on context it is used func (m *LogInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - var res []error - - if err := m.contextValidateSignedTreeHead(ctx, formats); err != nil { - res = append(res, err) - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -func (m *LogInfo) contextValidateSignedTreeHead(ctx context.Context, formats strfmt.Registry) error { - - if m.SignedTreeHead != nil { - if err := m.SignedTreeHead.ContextValidate(ctx, formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("signedTreeHead") - } - return err - } - } - return nil } @@ -161,96 +129,3 @@ func (m *LogInfo) UnmarshalBinary(b []byte) error { *m = res return nil } - -// LogInfoSignedTreeHead The current signed tree head -// -// swagger:model LogInfoSignedTreeHead -type LogInfoSignedTreeHead struct { - - // Key hint - // Required: true - // Format: byte - KeyHint *strfmt.Base64 `json:"keyHint"` - - // Log root - // Required: true - // Format: byte - LogRoot *strfmt.Base64 `json:"logRoot"` - - // Signature for log root - // Required: true - // Format: byte - Signature *strfmt.Base64 `json:"signature"` -} - -// Validate validates this log info signed tree head -func (m *LogInfoSignedTreeHead) Validate(formats strfmt.Registry) error { - var res []error - - if err := m.validateKeyHint(formats); err != nil { - res = append(res, err) - } - - if err := m.validateLogRoot(formats); err != nil { - res = append(res, err) - } - - if err := m.validateSignature(formats); err != nil { - res = append(res, err) - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -func (m *LogInfoSignedTreeHead) validateKeyHint(formats strfmt.Registry) error { - - if err := validate.Required("signedTreeHead"+"."+"keyHint", "body", m.KeyHint); err != nil { - return err - } - - return nil -} - -func (m *LogInfoSignedTreeHead) validateLogRoot(formats strfmt.Registry) error { - - if err := validate.Required("signedTreeHead"+"."+"logRoot", "body", m.LogRoot); err != nil { - return err - } - - return nil -} - -func (m *LogInfoSignedTreeHead) validateSignature(formats strfmt.Registry) error { - - if err := validate.Required("signedTreeHead"+"."+"signature", "body", m.Signature); err != nil { - return err - } - - return nil -} - -// ContextValidate validates this log info signed tree head based on context it is used -func (m *LogInfoSignedTreeHead) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} - -// MarshalBinary interface implementation -func (m *LogInfoSignedTreeHead) MarshalBinary() ([]byte, error) { - if m == nil { - return nil, nil - } - return swag.WriteJSON(m) -} - -// UnmarshalBinary interface implementation -func (m *LogInfoSignedTreeHead) UnmarshalBinary(b []byte) error { - var res LogInfoSignedTreeHead - if err := swag.ReadJSON(b, &res); err != nil { - return err - } - *m = res - return nil -} diff --git a/pkg/generated/restapi/configure_rekor_server.go b/pkg/generated/restapi/configure_rekor_server.go index 0d22cfe..262d71e 100644 --- a/pkg/generated/restapi/configure_rekor_server.go +++ b/pkg/generated/restapi/configure_rekor_server.go @@ -94,6 +94,8 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler { api.TimestampGetTimestampResponseHandler = timestamp.GetTimestampResponseHandlerFunc(pkgapi.TimestampResponseHandler) api.TimestampGetTimestampCertChainHandler = timestamp.GetTimestampCertChainHandlerFunc(pkgapi.GetTimestampCertChainHandler) + api.RegisterFormat("signedCheckpoint", &util.SignedCheckpoint{}, util.SignedCheckpointValidator) + api.PreServerShutdown = func() {} api.ServerShutdown = func() {} diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go index 0a1bbe1..ab2d4da 100644 --- a/pkg/generated/restapi/embedded_spec.go +++ b/pkg/generated/restapi/embedded_spec.go @@ -523,29 +523,8 @@ func init() { }, "signedTreeHead": { "description": "The current signed tree head", - "type": "object", - "required": [ - "keyHint", - "logRoot", - "signature" - ], - "properties": { - "keyHint": { - "description": "Key hint", - "type": "string", - "format": "byte" - }, - "logRoot": { - "description": "Log root", - "type": "string", - "format": "byte" - }, - "signature": { - "description": "Signature for log root", - "type": "string", - "format": "byte" - } - } + "type": "string", + "format": "signedCheckpoint" }, "treeSize": { "description": "The current number of nodes in the merkle tree", @@ -1496,29 +1475,8 @@ func init() { }, "signedTreeHead": { "description": "The current signed tree head", - "type": "object", - "required": [ - "keyHint", - "logRoot", - "signature" - ], - "properties": { - "keyHint": { - "description": "Key hint", - "type": "string", - "format": "byte" - }, - "logRoot": { - "description": "Log root", - "type": "string", - "format": "byte" - }, - "signature": { - "description": "Signature for log root", - "type": "string", - "format": "byte" - } - } + "type": "string", + "format": "signedCheckpoint" }, "treeSize": { "description": "The current number of nodes in the merkle tree", @@ -1527,32 +1485,6 @@ func init() { } } }, - "LogInfoSignedTreeHead": { - "description": "The current signed tree head", - "type": "object", - "required": [ - "keyHint", - "logRoot", - "signature" - ], - "properties": { - "keyHint": { - "description": "Key hint", - "type": "string", - "format": "byte" - }, - "logRoot": { - "description": "Log root", - "type": "string", - "format": "byte" - }, - "signature": { - "description": "Signature for log root", - "type": "string", - "format": "byte" - } - } - }, "ProposedEntry": { "type": "object", "required": [ diff --git a/pkg/util/checkpoint.go b/pkg/util/checkpoint.go new file mode 100644 index 0000000..d16f0f0 --- /dev/null +++ b/pkg/util/checkpoint.go @@ -0,0 +1,319 @@ +// +// Copyright 2021 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 util + +import ( + "bufio" + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" + "golang.org/x/mod/sumdb/note" +) + +// heavily borrowed from https://github.com/google/trillian-examples/blob/master/formats/log/checkpoint.go + +type Checkpoint struct { + // Ecosystem is the ecosystem/version string + Ecosystem string + // Size is the number of entries in the log at this checkpoint. + Size uint64 + // Hash is the hash which commits to the contents of the entire log. + Hash []byte + // OtherContent is any additional data to be included in the signed payload; each element is assumed to be one line + OtherContent []string +} + +// String returns the String representation of the Checkpoint +func (c Checkpoint) String() string { + var b strings.Builder + fmt.Fprintf(&b, "%s\n%d\n%s\n", c.Ecosystem, c.Size, base64.StdEncoding.EncodeToString(c.Hash)) + for _, line := range c.OtherContent { + fmt.Fprintf(&b, "%s\n", line) + } + return b.String() +} + +// MarshalText returns the common format representation of this Checkpoint. +func (c Checkpoint) MarshalText() ([]byte, error) { + return []byte(c.String()), nil +} + +// UnmarshalText parses the common formatted checkpoint data and stores the result +// in the Checkpoint. +// +// The supplied data is expected to begin with the following 3 lines of text, +// each followed by a newline: +// <ecosystem/version string> +// <decimal representation of log size> +// <base64 representation of root hash> +// <optional non-empty line of other content>... +// <optional non-empty line of other content>... +// +// This will discard any content found after the checkpoint (including signatures) +func (c *Checkpoint) UnmarshalText(data []byte) error { + l := bytes.Split(data, []byte("\n")) + if len(l) < 4 { + return errors.New("invalid checkpoint - too few newlines") + } + eco := string(l[0]) + if len(eco) == 0 { + return errors.New("invalid checkpoint - empty ecosystem") + } + size, err := strconv.ParseUint(string(l[1]), 10, 64) + if err != nil { + return fmt.Errorf("invalid checkpoint - size invalid: %w", err) + } + h, err := base64.StdEncoding.DecodeString(string(l[2])) + if err != nil { + return fmt.Errorf("invalid checkpoint - invalid hash: %w", err) + } + *c = Checkpoint{ + Ecosystem: eco, + Size: size, + Hash: h, + } + if len(l) >= 5 { + for _, line := range l[3:] { + if len(line) == 0 { + break + } + c.OtherContent = append(c.OtherContent, string(line)) + } + } + return nil +} + +func (c Checkpoint) Sign(identity string, signer crypto.Signer, opts crypto.SignerOpts) (*note.Signature, error) { + hf := crypto.SHA256 + if opts != nil { + hf = opts.HashFunc() + } + + input, _ := c.MarshalText() + var digest []byte + if hf != crypto.Hash(0) { + hasher := hf.New() + _, err := hasher.Write(input) + if err != nil { + return nil, errors.Wrap(err, "hashing checkpoint before signing") + } + digest = hasher.Sum(nil) + } else { + digest, _ = c.MarshalText() + } + + sig, err := signer.Sign(rand.Reader, digest, opts) + if err != nil { + return nil, errors.Wrap(err, "signing checkpoint") + } + pubKeyBytes, err := x509.MarshalPKIXPublicKey(signer.Public()) + if err != nil { + return nil, errors.Wrap(err, "marshalling public key") + } + + pkSha := sha256.Sum256(pubKeyBytes) + + signature := note.Signature{ + Name: identity, + Hash: binary.BigEndian.Uint32(pkSha[:]), + Base64: base64.StdEncoding.EncodeToString(sig), + } + + return &signature, nil + +} + +type SignedCheckpoint struct { + Checkpoint + // Signatures are one or more signature lines covering the payload + Signatures []note.Signature +} + +// String returns the String representation of the SignedCheckpoint +func (s SignedCheckpoint) String() string { + var b strings.Builder + b.WriteString(s.Checkpoint.String()) + b.WriteRune('\n') + for _, sig := range s.Signatures { + var hbuf [4]byte + binary.BigEndian.PutUint32(hbuf[:], sig.Hash) + sigBytes, _ := base64.StdEncoding.DecodeString(sig.Base64) + b64 := base64.StdEncoding.EncodeToString(append(hbuf[:], sigBytes...)) + fmt.Fprintf(&b, "%c %s %s\n", '\u2014', sig.Name, b64) + } + + return b.String() +} + +// UnmarshalText parses the common formatted checkpoint data and stores the result +// in the SignedCheckpoint. THIS DOES NOT VERIFY SIGNATURES INSIDE THE CONTENT! +// +// The supplied data is expected to contain a single Checkpoint, followed by a single +// line with no comment, followed by one or more lines with the following format: +// +// \u2014 name signature +// +// * name is the string associated with the signer +// * signature is a base64 encoded string; the first 4 bytes of the decoded value is a +// hint to the public key; it is a big-endian encoded uint32 representing the first +// 4 bytes of the SHA256 hash of the public key +func (s *SignedCheckpoint) UnmarshalText(data []byte) error { + sc := SignedCheckpoint{} + + if err := sc.Checkpoint.UnmarshalText(data); err != nil { + return errors.Wrap(err, "parsing checkpoint portion") + } + + b := bufio.NewScanner(bytes.NewReader(data)) + var pastCheckpoint bool + for b.Scan() { + if len(b.Text()) == 0 { + pastCheckpoint = true + continue + } + if pastCheckpoint { + var name, signature string + if _, err := fmt.Fscanf(strings.NewReader(b.Text()), "\u2014 %s %s\n", &name, &signature); err != nil { + return errors.Wrap(err, "parsing signature") + } + + sigBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return errors.Wrap(err, "decoding signature") + } + if len(sigBytes) < 5 { + return errors.New("signature is too small") + } + + sig := note.Signature{ + Name: name, + Hash: binary.BigEndian.Uint32(sigBytes[0:4]), + Base64: base64.StdEncoding.EncodeToString(sigBytes[4:]), + } + sc.Signatures = append(sc.Signatures, sig) + } + } + if len(sc.Signatures) == 0 { + return errors.New("no signatures found in input") + } + + // copy sc to s + *s = sc + return nil +} + +// Verify checks that one of the signatures can be successfully verified using +// the supplied public key +func (s SignedCheckpoint) Verify(public crypto.PublicKey) bool { + if len(s.Signatures) == 0 { + return false + } + + msg, _ := s.Checkpoint.MarshalText() + //TODO: generalize this + digest := sha256.Sum256(msg) + + for _, s := range s.Signatures { + sigBytes, err := base64.StdEncoding.DecodeString(s.Base64) + if err != nil { + return false + } + switch pk := public.(type) { + case *rsa.PublicKey: + if err := rsa.VerifyPSS(pk, crypto.SHA256, digest[:], sigBytes, &rsa.PSSOptions{Hash: crypto.SHA256}); err == nil { + return true + } + case *ecdsa.PublicKey: + if ecdsa.VerifyASN1(pk, digest[:], sigBytes) { + return true + } + case *ed25519.PublicKey: + if ed25519.Verify(*pk, msg, sigBytes) { + return true + } + default: + return false + } + } + return false +} + +// Sign adds an additional signature to a SignedCheckpoint object +// The signature is added to the signature array as well as being directly returned to the caller +func (s *SignedCheckpoint) Sign(identity string, signer crypto.Signer, opts crypto.SignerOpts) (*note.Signature, error) { + sig, err := s.Checkpoint.Sign(identity, signer, opts) + if err != nil { + return nil, err + } + s.Signatures = append(s.Signatures, *sig) + return sig, nil +} + +// MarshalText returns the common format representation of this SignedCheckpoint. +func (s SignedCheckpoint) MarshalText() ([]byte, error) { + return []byte(s.String()), nil +} + +func SignedCheckpointValidator(strToValidate string) bool { + s := SignedCheckpoint{} + return s.UnmarshalText([]byte(strToValidate)) == nil +} + +func CheckpointValidator(strToValidate string) bool { + c := Checkpoint{} + return c.UnmarshalText([]byte(strToValidate)) == nil +} + +type RekorSTH struct { + SignedCheckpoint +} + +func (r *RekorSTH) SetTimestamp(timestamp uint64) { + var ts uint64 + for i, val := range r.OtherContent { + if n, _ := fmt.Fscanf(strings.NewReader(val), "Timestamp: %d", &ts); n == 1 { + r.OtherContent = append(r.OtherContent[:i], r.OtherContent[i+1:]...) + } + } + r.OtherContent = append(r.OtherContent, fmt.Sprintf("Timestamp: %d", timestamp)) +} + +func (r *RekorSTH) GetTimestamp() uint64 { + var ts uint64 + for _, val := range r.OtherContent { + if n, _ := fmt.Fscanf(strings.NewReader(val), "Timestamp: %d", &ts); n == 1 { + break + } + } + return ts +} + +func RekorSTHValidator(strToValidate string) bool { + r := RekorSTH{} + return r.UnmarshalText([]byte(strToValidate)) == nil +} diff --git a/pkg/util/checkpoint_test.go b/pkg/util/checkpoint_test.go new file mode 100644 index 0000000..2a91a43 --- /dev/null +++ b/pkg/util/checkpoint_test.go @@ -0,0 +1,443 @@ +// +// Copyright 2021 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 util + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/mod/sumdb/note" +) + +// heavily borrowed from https://github.com/google/trillian-examples/blob/master/formats/log/checkpoint_test.go + +func TestMarshalCheckpoint(t *testing.T) { + for _, test := range []struct { + c Checkpoint + want string + }{ + { + c: Checkpoint{ + Ecosystem: "Log Checkpoint v0", + Size: 123, + Hash: []byte("bananas"), + }, + want: "Log Checkpoint v0\n123\nYmFuYW5hcw==\n", + }, { + c: Checkpoint{ + Ecosystem: "Banana Checkpoint v5", + Size: 9944, + Hash: []byte("the view from the tree tops is great!"), + }, + want: "Banana Checkpoint v5\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n", + }, { + c: Checkpoint{ + Ecosystem: "Banana Checkpoint v7", + Size: 9943, + Hash: []byte("the view from the tree tops is great!"), + OtherContent: []string{"foo", "bar"}, + }, + want: "Banana Checkpoint v7\n9943\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\nfoo\nbar\n", + }, + } { + t.Run(string(test.c.Hash), func(t *testing.T) { + got, err := test.c.MarshalText() + if err != nil { + t.Fatalf("unexpected error marshalling: %v", err) + } + if string(got) != test.want { + t.Fatalf("Marshal = %q, want %q", got, test.want) + } + }) + } +} + +func TestUnmarshalCheckpoint(t *testing.T) { + for _, test := range []struct { + desc string + m string + want Checkpoint + wantErr bool + }{ + { + desc: "valid one", + m: "Log Checkpoint v0\n123\nYmFuYW5hcw==\n", + want: Checkpoint{ + Ecosystem: "Log Checkpoint v0", + Size: 123, + Hash: []byte("bananas"), + }, + }, { + desc: "valid with different ecosystem", + m: "Banana Checkpoint v1\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n", + want: Checkpoint{ + Ecosystem: "Banana Checkpoint v1", + Size: 9944, + Hash: []byte("the view from the tree tops is great!"), + }, + }, { + desc: "valid with trailing data", + m: "Log Checkpoint v0\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\nHere's some associated data.\n", + want: Checkpoint{ + Ecosystem: "Log Checkpoint v0", + Size: 9944, + Hash: []byte("the view from the tree tops is great!"), + OtherContent: []string{"Here's some associated data."}, + }, + }, { + desc: "valid with multiple trailing data lines", + m: "Log Checkpoint v0\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\nlots\nof\nlines\n", + want: Checkpoint{ + Ecosystem: "Log Checkpoint v0", + Size: 9944, + Hash: []byte("the view from the tree tops is great!"), + OtherContent: []string{"lots", "of", "lines"}, + }, + }, { + desc: "valid with trailing newlines", + m: "Log Checkpoint v0\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n\n\n\n", + want: Checkpoint{ + Ecosystem: "Log Checkpoint v0", + Size: 9944, + Hash: []byte("the view from the tree tops is great!"), + }, + }, { + desc: "invalid - insufficient lines", + m: "Head\n9944\n", + wantErr: true, + }, { + desc: "invalid - empty header", + m: "\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n", + wantErr: true, + }, { + desc: "invalid - missing newline on roothash", + m: "Log Checkpoint v0\n123\nYmFuYW5hcw==", + wantErr: true, + }, { + desc: "invalid size - not a number", + m: "Log Checkpoint v0\nbananas\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n", + wantErr: true, + }, { + desc: "invalid size - negative", + m: "Log Checkpoint v0\n-34\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n", + wantErr: true, + }, { + desc: "invalid size - too large", + m: "Log Checkpoint v0\n3438945738945739845734895735\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n", + wantErr: true, + }, { + desc: "invalid roothash - not base64", + m: "Log Checkpoint v0\n123\nThisIsn'tBase64\n", + wantErr: true, + }, + } { + t.Run(string(test.desc), func(t *testing.T) { + var got Checkpoint + var gotErr error + if gotErr = got.UnmarshalText([]byte(test.m)); (gotErr != nil) != test.wantErr { + t.Fatalf("Unmarshal = %q, wantErr: %T", gotErr, test.wantErr) + } + if diff := cmp.Diff(test.want, got); len(diff) != 0 { + t.Fatalf("Unmarshalled Checkpoint with diff %s", diff) + } + if !test.wantErr != CheckpointValidator(test.m) { + t.Fatalf("Validator failed for %s", test.desc) + } + }) + } +} + +func TestSigningRoundtripCheckpoint(t *testing.T) { + rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) + ecdsaKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + edPubKey, edPrivKey, _ := ed25519.GenerateKey(rand.Reader) + for _, test := range []struct { + c Checkpoint + identity string + signer crypto.Signer + pubKey crypto.PublicKey + opts crypto.SignerOpts + wantSignErr bool + wantVerifyErr bool + }{ + { + c: Checkpoint{ + Ecosystem: "Log Checkpoint RSA v0", + Size: 123, + Hash: []byte("bananas"), + }, + identity: "someone", + signer: rsaKey, + pubKey: rsaKey.Public(), + opts: &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthAuto, Hash: crypto.SHA256}, + wantSignErr: false, + wantVerifyErr: false, + }, + { + c: Checkpoint{ + Ecosystem: "Log Checkpoint ECDSA v0", + Size: 123, + Hash: []byte("bananas"), + }, + identity: "someone", + signer: ecdsaKey, + pubKey: ecdsaKey.Public(), + opts: nil, + wantSignErr: false, + wantVerifyErr: false, + }, + { + c: Checkpoint{ + Ecosystem: "Log Checkpoint Ed25519 v0", + Size: 123, + Hash: []byte("bananas"), + }, + identity: "someone", + signer: edPrivKey, + pubKey: &edPubKey, + opts: crypto.Hash(0), + wantSignErr: false, + wantVerifyErr: false, + }, + { + c: Checkpoint{ + Ecosystem: "Log Checkpoint Mismatch v0", + Size: 123, + Hash: []byte("bananas"), + }, + identity: "someone", + signer: edPrivKey, + pubKey: ecdsaKey.Public(), + opts: crypto.Hash(0), + wantSignErr: false, + wantVerifyErr: true, + }, + { + c: Checkpoint{ + Ecosystem: "Log Checkpoint Mismatch v1", + Size: 123, + Hash: []byte("bananas"), + }, + identity: "someone", + signer: ecdsaKey, + pubKey: rsaKey.Public(), + opts: &rsa.PSSOptions{Hash: crypto.SHA256}, + wantSignErr: false, + wantVerifyErr: true, + }, + { + c: Checkpoint{ + Ecosystem: "Log Checkpoint Mismatch v2", + Size: 123, + Hash: []byte("bananas"), + }, + identity: "someone", + signer: edPrivKey, + pubKey: rsaKey.Public(), + opts: crypto.Hash(0), + wantSignErr: false, + wantVerifyErr: true, + }, + { + c: Checkpoint{ + Ecosystem: "Log Checkpoint Mismatch v3", + Size: 123, + Hash: []byte("bananas"), + }, + identity: "someone", + signer: ecdsaKey, + pubKey: &edPubKey, + opts: nil, + wantSignErr: false, + wantVerifyErr: true, + }, + } { + t.Run(string(test.c.Ecosystem), func(t *testing.T) { + sig, err := test.c.Sign(test.identity, test.signer, test.opts) + if (err != nil) != test.wantSignErr { + t.Fatalf("signing test failed: wantSignErr %v, err %v", test.wantSignErr, err) + } + if !test.wantSignErr { + sc := SignedCheckpoint{ + Checkpoint: test.c, + Signatures: []note.Signature{ + *sig, + }, + } + if !sc.Verify(test.pubKey) != test.wantVerifyErr { + t.Fatalf("verification test failed %v", sc.Verify(test.pubKey)) + } + if _, err := sc.Sign("second", test.signer, test.opts); err != nil { + t.Fatalf("adding second signature failed: %v", err) + } + if len(sc.Signatures) != 2 { + t.Fatalf("expected two signatures on checkpoint, only found %v", len(sc.Signatures)) + } + // finally, test marshalling object and unmarshalling + marshalledSc, err := sc.MarshalText() + if err != nil { + t.Fatalf("error during marshalling: %v", err) + } + sc2 := SignedCheckpoint{} + if err := sc2.UnmarshalText(marshalledSc); err != nil { + t.Fatalf("error unmarshalling just marshalled object %v\n%v", err, string(marshalledSc)) + } + if diff := cmp.Diff(sc, sc2); len(diff) != 0 { + t.Fatalf("UnmarshalText = diff %s", diff) + } + } + }) + } +} + +func TestInvalidSigVerification(t *testing.T) { + for _, test := range []struct { + sc SignedCheckpoint + pubKey crypto.PublicKey + expectedResult bool + }{ + { + sc: SignedCheckpoint{ + Checkpoint: Checkpoint{ + Ecosystem: "Log Checkpoint v0", + Size: 123, + Hash: []byte("bananas"), + }, + Signatures: []note.Signature{}, + }, + expectedResult: false, + }, + { + sc: SignedCheckpoint{ + Checkpoint: Checkpoint{ + Ecosystem: "Log Checkpoint v0", + Size: 123, + Hash: []byte("bananas"), + }, + Signatures: []note.Signature{ + { + Name: "something", + Hash: 1234, + Base64: "not_base 64 string", + }, + }, + }, + expectedResult: false, + }, + { + sc: SignedCheckpoint{ + Checkpoint: Checkpoint{ + Ecosystem: "Log Checkpoint v0", + Size: 123, + Hash: []byte("bananas"), + }, + Signatures: []note.Signature{ + { + Name: "someone", + Hash: 142, + Base64: "bm90IGEgc2ln", // valid base64, not a valid signature + }, + }, + }, + expectedResult: false, + }, + { + sc: SignedCheckpoint{ + Checkpoint: Checkpoint{ + Ecosystem: "Log Checkpoint Ed25519 v0", + Size: 123, + Hash: []byte("bananas"), + }, + Signatures: []note.Signature{ + { + Name: "someone", + Hash: 1390313051, + Base64: "pOhM+S/mYjEYtQsOF4lL8o/dR+nbjoz5Cvg/n486KIismpVq0s4wxBaakmryI7zThjWAqRUyECPL3WSEcVDEBQ==", + }, + }, + }, + pubKey: nil, // valid input, invalid key + expectedResult: false, + }, + } { + t.Run(string(test.sc.Ecosystem), func(t *testing.T) { + result := test.sc.Verify(test.pubKey) + if result != test.expectedResult { + t.Fatal("verification test generated unexpected result") + } + }) + } +} + +// does not test validity of signatures but merely parsing logic +func TestUnmarshalSignedCheckpoint(t *testing.T) { + for _, test := range []struct { + desc string + m string + wantErr bool + }{ + { + desc: "invalid checkpoint, no signatures", + m: "Log Checkpoint v0\n\nYmFuYW5hcw==\n\n", + wantErr: true, + }, { + desc: "valid checkpoint, no signatures", + m: "Log Checkpoint v0\n123\nYmFuYW5hcw==\n\n", + wantErr: true, + }, { + desc: "incorrect signature line format", + m: "Banana Checkpoint v1\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n\n* name not-a-sig\n", + wantErr: true, + }, { + desc: "signature not base64 encoded", + m: "Banana Checkpoint v1\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n\n\u2014 name not-b64\n", + wantErr: true, + }, { + desc: "missing identity", + m: "Banana Checkpoint v1\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n\n\u2014 YQ==\n", + wantErr: true, + }, { + desc: "signature base64 encoded but too short", + m: "Banana Checkpoint v1\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n\n\u2014 name YQ==\n", + wantErr: true, + }, { + desc: "valid signed checkpoint - single signature", + m: "Banana Checkpoint v1\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n\n\u2014 name pOhM+S/mYjEYtQsOF4lL8o/dR+nbjoz5Cvg/n486KIismpVq0s4wxBaakmryI7zThjWAqRUyECPL3WSEcVDEBQ==\n", + wantErr: false, + }, { + desc: "valid signed checkpoint - two signatures", + m: "Banana Checkpoint v1\n9944\ndGhlIHZpZXcgZnJvbSB0aGUgdHJlZSB0b3BzIGlzIGdyZWF0IQ==\n\n\u2014 name pOhM+S/mYjEYtQsOF4lL8o/dR+nbjoz5Cvg/n486KIismpVq0s4wxBaakmryI7zThjWAqRUyECPL3WSEcVDEBQ==\n\u2014 another_name pOhM+S/mYjEYtQsOF4lL8o/dR+nbjoz5Cvg/n486KIismpVq0s4wxBaakmryI7zThjWAqRUyECPL3WSEcVDEBQ==\n", + wantErr: false, + }, + } { + t.Run(string(test.desc), func(t *testing.T) { + var got SignedCheckpoint + var gotErr error + if gotErr = got.UnmarshalText([]byte(test.m)); (gotErr != nil) != test.wantErr { + t.Fatalf("UnmarshalText(%s) = %q, wantErr: %v", test.desc, gotErr, test.wantErr) + } + if !test.wantErr != SignedCheckpointValidator(test.m) { + t.Fatalf("Validator failed for %s", test.desc) + } + }) + } +} diff --git a/pkg/verify/log_root.go b/pkg/verify/log_root.go deleted file mode 100644 index 20c0a7b..0000000 --- a/pkg/verify/log_root.go +++ /dev/null @@ -1,65 +0,0 @@ -// -// Copyright 2021 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 ( - "crypto" - "crypto/ecdsa" - "fmt" - - "github.com/google/trillian/types" - "github.com/pkg/errors" -) - -// this verification copied from https://github.com/google/trillian/blob/v1.3.13/crypto/verifier.go -// which has since been deleted - -// SignedLogRoot verifies the signed log root and returns its contents -func SignedLogRoot(pub crypto.PublicKey, logRoot, logRootSignature []byte) (*types.LogRootV1, error) { - hash := crypto.SHA256 - if err := verify(pub, hash, logRoot, logRootSignature); err != nil { - return nil, err - } - - var lr types.LogRootV1 - if err := lr.UnmarshalBinary(logRoot); err != nil { - return nil, err - } - return &lr, nil -} - -// verify cryptographically verifies the output of Signer. -func verify(pub crypto.PublicKey, hasher crypto.Hash, data, sig []byte) error { - if sig == nil { - return errors.New("signature is nil") - } - - h := hasher.New() - if _, err := h.Write(data); err != nil { - return errors.Wrap(err, "write") - } - digest := h.Sum(nil) - - switch pub := pub.(type) { - case *ecdsa.PublicKey: - if !ecdsa.VerifyASN1(pub, digest, sig) { - return errors.New("verification failed") - } - default: - return fmt.Errorf("unknown public key type: %T", pub) - } - return nil -} diff --git a/pkg/verify/log_root_test.go b/pkg/verify/log_root_test.go deleted file mode 100644 index 3ce72ad..0000000 --- a/pkg/verify/log_root_test.go +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright 2021 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" - "crypto" - "testing" - - "github.com/sigstore/rekor/pkg/signer" -) - -func TestVerify(t *testing.T) { - signer, err := signer.NewMemory() - if err != nil { - t.Fatalf("getting signer: %v", signer) - } - - // sign and verify - ctx := context.Background() - msg := []byte("foo") - signature, _, err := signer.Sign(ctx, msg) - if err != nil { - t.Fatalf("signing: %v", err) - } - - // get public key - pubKey, err := signer.PublicKey(ctx) - if err != nil { - t.Fatalf("getting public key: %v", err) - } - - // verify should work with correct signature - if err := verify(pubKey, crypto.SHA256, msg, signature); err != nil { - t.Fatalf("error verifying: %v", err) - } - - // and fail with an incorrect signature - if err := verify(pubKey, crypto.SHA256, msg, []byte("nope")); err == nil { - t.Fatalf("expected failure with incorrect signature") - } -} -- GitLab