diff --git a/go.mod b/go.mod index 69da80e45d21c911a48c79f14d5acd0dc22c7d80..7e5271f7847104ad370070ff391478dae3e32e33 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/projectrekor/rekor -go 1.15 +go 1.14 require ( github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef @@ -33,6 +33,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 github.com/tidwall/sjson v1.1.2 + github.com/urfave/negroni v1.0.0 github.com/xeipuuv/gojsonschema v1.2.0 go.etcd.io/etcd v3.3.25+incompatible // indirect go.uber.org/goleak v1.1.10 diff --git a/go.sum b/go.sum index bcd46a9838a9983b53b6932f9bdc7280c9ca9b68..126b6194a98efb7c8ede8a2648ca6310b37367f8 100644 --- a/go.sum +++ b/go.sum @@ -760,6 +760,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= diff --git a/openapi.yaml b/openapi.yaml index 73adb4e7b17e3991e596a3bb1b615865c991b294..2a1524dce242b53de6dfbb5202bfe4a0c452df13 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -127,6 +127,7 @@ paths: name: entryUUID type: string required: true + pattern: '^[0-9a-fA-F]{64}$' description: the UUID of the entry to be retrieved from the log. The UUID is also the merkle tree hash of the entry. responses: 200: @@ -150,6 +151,7 @@ paths: name: entryUUID type: string required: true + pattern: '^[0-9a-fA-F]{64}$' description: the UUID of the entry for which the inclusion proof information should be returned responses: 200: @@ -234,6 +236,7 @@ definitions: items: type: string minItems: 1 + pattern: '^[0-9a-fA-F]{64}$' logIndexes: type: array minItems: 1 @@ -309,7 +312,7 @@ definitions: Error: type: object properties: - status: + code: type: integer message: type: string diff --git a/pkg/api/api.go b/pkg/api/api.go index bb2a38e28e27fa192a25eeb29b87cf55af21df61..092d9dd5dd08cbe8b2580bc480a9a43b6b9140ac 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -19,6 +19,7 @@ package api import ( "context" "fmt" + "net/http" "time" "github.com/google/trillian" @@ -73,7 +74,23 @@ func NewAPI(ctx context.Context) (*API, error) { } return &API{ - client: TrillianClientInstance(logClient, tLogID), + client: TrillianClientInstance(logClient, tLogID, ctx), pubkey: t.PublicKey, }, nil } + +type ctxKeyRekorAPI int + +const rekorAPILookupKey ctxKeyRekorAPI = 0 + +func AddAPIToContext(ctx context.Context) (context.Context, error) { + api, err := NewAPI(ctx) + if err != nil { + return nil, err + } + return context.WithValue(ctx, rekorAPILookupKey, api), nil +} + +func apiFromRequest(r *http.Request) *API { + return r.Context().Value(rekorAPILookupKey).(*API) +} diff --git a/pkg/api/entries.go b/pkg/api/entries.go index 6671e65616ac5075dec363422a03c8d59b0533ae..7a4ae2c51045843f386c04037d73eec86e32faa4 100644 --- a/pkg/api/entries.go +++ b/pkg/api/entries.go @@ -19,9 +19,12 @@ import ( "crypto" "crypto/x509" "encoding/hex" + "errors" "fmt" "net/http" + "github.com/google/trillian" + "github.com/go-openapi/swag" "google.golang.org/grpc/codes" @@ -39,29 +42,23 @@ import ( ) func GetLogEntryByIndexHandler(params entries.GetLogEntryByIndexParams) middleware.Responder { - api, _ := NewAPI(params.HTTPRequest.Context()) + httpReq := params.HTTPRequest + api := apiFromRequest(httpReq) - indexes := []int64{params.LogIndex} - resp, err := api.client.getLeafByIndex(indexes) - if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryByIndexDefault(code), code, err, trillianCommunicationError, params.HTTPRequest) - } + resp := api.client.getLeafByIndex(params.LogIndex) switch resp.status { case codes.OK: case codes.NotFound, codes.OutOfRange: - return logAndReturnError(entries.NewGetLogEntryByIndexNotFound(), http.StatusNotFound, nil, "", params.HTTPRequest) + return handleRekorAPIError(params, http.StatusNotFound, fmt.Errorf("grpc error: %w", resp.err), "") default: - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryByIndexDefault(code), code, nil, trillianCommunicationError, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc err: %w", resp.err), trillianCommunicationError) } - leaves := resp.getLeafByIndexResult.GetLeaves() + leaves := resp.getLeafByRangeResult.GetLeaves() if len(leaves) > 1 { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryByIndexDefault(code), code, nil, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("len(leaves): %v", len(leaves)), trillianUnexpectedResult) } else if len(leaves) == 0 { - return logAndReturnError(entries.NewGetLogEntryByIndexNotFound(), http.StatusNotFound, nil, "", params.HTTPRequest) + return handleRekorAPIError(params, http.StatusNotFound, errors.New("grpc returned 0 leaves with success code"), "") } leaf := leaves[0] @@ -75,30 +72,26 @@ func GetLogEntryByIndexHandler(params entries.GetLogEntryByIndexParams) middlewa } func CreateLogEntryHandler(params entries.CreateLogEntryParams) middleware.Responder { + httpReq := params.HTTPRequest entry, err := types.NewEntry(params.ProposedEntry) if err != nil { - return logAndReturnError(entries.NewCreateLogEntryBadRequest(), http.StatusBadRequest, err, err.Error(), params.HTTPRequest) + return handleRekorAPIError(params, http.StatusBadRequest, err, err.Error()) } - leaf, err := entry.Canonicalize(params.HTTPRequest.Context()) + leaf, err := entry.Canonicalize(httpReq.Context()) if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewCreateLogEntryDefault(code), code, err, failedToGenerateCanonicalEntry, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, err, failedToGenerateCanonicalEntry) } - api, _ := NewAPI(params.HTTPRequest.Context()) - resp, err := api.client.addLeaf(leaf) - if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewCreateLogEntryDefault(code), code, err, trillianCommunicationError, params.HTTPRequest) - } + api := apiFromRequest(httpReq) + + resp := api.client.addLeaf(leaf) switch resp.status { case codes.OK: case codes.AlreadyExists, codes.FailedPrecondition: - return logAndReturnError(entries.NewCreateLogEntryConflict(), http.StatusConflict, nil, entryAlreadyExists, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusConflict, fmt.Errorf("grpc error: %w", resp.err), entryAlreadyExists) default: - code := http.StatusInternalServerError - return logAndReturnError(entries.NewCreateLogEntryDefault(code), code, nil, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc error: %w", resp.err), trillianUnexpectedResult) } queuedLeaf := resp.getAddResult.QueuedLeaf.Leaf @@ -112,34 +105,30 @@ func CreateLogEntryHandler(params entries.CreateLogEntryParams) middleware.Respo }, } - location := strfmt.URI(fmt.Sprintf("%v/%v", params.HTTPRequest.URL, uuid)) + location := strfmt.URI(fmt.Sprintf("%v/%v", httpReq.URL, uuid)) return entries.NewCreateLogEntryCreated().WithPayload(logEntry).WithLocation(location).WithETag(uuid) } func GetLogEntryByUUIDHandler(params entries.GetLogEntryByUUIDParams) middleware.Responder { - api, _ := NewAPI(params.HTTPRequest.Context()) + httpReq := params.HTTPRequest + api := apiFromRequest(httpReq) hashValue, _ := hex.DecodeString(params.EntryUUID) hashes := [][]byte{hashValue} - resp, err := api.client.getLeafByHash(hashes) - if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryByUUIDDefault(code), code, err, trillianCommunicationError, params.HTTPRequest) - } + + resp := api.client.getLeafByHash(hashes) // TODO: if this API is deprecated, we need to ask for inclusion proof and then use index in proof result to get leaf switch resp.status { case codes.OK: case codes.NotFound: - return logAndReturnError(entries.NewGetLogEntryByUUIDNotFound(), http.StatusNotFound, nil, "", params.HTTPRequest) + return handleRekorAPIError(params, http.StatusNotFound, fmt.Errorf("grpc error: %w", resp.err), "") default: - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryByUUIDDefault(code), code, nil, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc error: %w", resp.err), trillianUnexpectedResult) } leaves := resp.getLeafResult.GetLeaves() if len(leaves) > 1 { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryByUUIDDefault(code), code, nil, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("len(leaves): %v", len(leaves)), trillianUnexpectedResult) } else if len(leaves) == 0 { - return logAndReturnError(entries.NewGetLogEntryByUUIDNotFound(), http.StatusNotFound, nil, "", params.HTTPRequest) + return handleRekorAPIError(params, http.StatusNotFound, errors.New("grpc returned 0 leaves with success code"), "") } leaf := leaves[0] @@ -155,39 +144,33 @@ func GetLogEntryByUUIDHandler(params entries.GetLogEntryByUUIDParams) middleware } func GetLogEntryProofHandler(params entries.GetLogEntryProofParams) middleware.Responder { - api, _ := NewAPI(params.HTTPRequest.Context()) + httpReq := params.HTTPRequest + api := apiFromRequest(httpReq) hashValue, _ := hex.DecodeString(params.EntryUUID) - resp, err := api.client.getProofByHash(hashValue) - if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryProofDefault(code), code, err, trillianCommunicationError, params.HTTPRequest) - } + + resp := api.client.getProofByHash(hashValue) switch resp.status { case codes.OK: case codes.NotFound: - return logAndReturnError(entries.NewGetLogEntryProofNotFound(), http.StatusNotFound, nil, "", params.HTTPRequest) + return handleRekorAPIError(params, http.StatusNotFound, fmt.Errorf("grpc error: %w", resp.err), "") default: - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryProofDefault(code), code, nil, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc error: %w", resp.err), trillianUnexpectedResult) } result := resp.getProofResult // validate result is signed with the key we're aware of pub, err := x509.ParsePKIXPublicKey(api.pubkey.Der) if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryProofDefault(code), code, err, http.StatusText(code), params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, err, "") } verifier := tclient.NewLogVerifier(rfc6962.DefaultHasher, pub, crypto.SHA256) root, err := tcrypto.VerifySignedLogRoot(verifier.PubKey, verifier.SigHash, result.SignedLogRoot) if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryProofDefault(code), code, err, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, err, trillianUnexpectedResult) } if len(result.Proof) != 1 { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewGetLogEntryProofDefault(code), code, nil, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("len(result.Proof) = %v", len(result.Proof)), trillianUnexpectedResult) } proof := result.Proof[0] @@ -207,7 +190,8 @@ func GetLogEntryProofHandler(params entries.GetLogEntryProofParams) middleware.R func SearchLogQueryHandler(params entries.SearchLogQueryParams) middleware.Responder { resultPayload := []models.LogEntry{} - api, _ := NewAPI(params.HTTPRequest.Context()) + httpReq := params.HTTPRequest + api := apiFromRequest(httpReq) //TODO: parallelize this into different goroutines to speed up search searchHashes := [][]byte{} @@ -215,8 +199,7 @@ func SearchLogQueryHandler(params entries.SearchLogQueryParams) middleware.Respo for _, uuid := range params.Entry.EntryUUIDs { hash, err := hex.DecodeString(uuid) if err != nil { - code := http.StatusBadRequest - return logAndReturnError(entries.NewSearchLogQueryBadRequest(), code, err, http.StatusText(code), params.HTTPRequest) + return handleRekorAPIError(params, http.StatusBadRequest, err, malformedUUID) } searchHashes = append(searchHashes, hash) } @@ -224,37 +207,29 @@ func SearchLogQueryHandler(params entries.SearchLogQueryParams) middleware.Respo for _, e := range params.Entry.Entries() { entry, err := types.NewEntry(e) if err != nil { - code := http.StatusBadRequest - return logAndReturnError(entries.NewSearchLogQueryBadRequest(), code, err, err.Error(), params.HTTPRequest) + return handleRekorAPIError(params, http.StatusBadRequest, err, err.Error()) } if entry.HasExternalEntities() { - if err := entry.FetchExternalEntities(params.HTTPRequest.Context()); err != nil { - code := http.StatusBadRequest - return logAndReturnError(entries.NewSearchLogQueryDefault(code), code, err, err.Error(), params.HTTPRequest) + if err := entry.FetchExternalEntities(httpReq.Context()); err != nil { + return handleRekorAPIError(params, http.StatusBadRequest, err, err.Error()) } } - leaf, err := entry.Canonicalize(params.HTTPRequest.Context()) + leaf, err := entry.Canonicalize(httpReq.Context()) if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewSearchLogQueryDefault(code), code, err, err.Error(), params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, err, err.Error()) } hasher := rfc6962.DefaultHasher leafHash := hasher.HashLeaf(leaf) searchHashes = append(searchHashes, leafHash) } - resp, err := api.client.getLeafByHash(searchHashes) - if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewSearchLogQueryDefault(code), code, err, trillianCommunicationError, params.HTTPRequest) - } + resp := api.client.getLeafByHash(searchHashes) // TODO: if this API is deprecated, we need to ask for inclusion proof and then use index in proof result to get leaf switch resp.status { case codes.OK, codes.NotFound: default: - code := http.StatusInternalServerError - return logAndReturnError(entries.NewSearchLogQueryDefault(code), code, nil, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc error: %w", resp.err), trillianUnexpectedResult) } for _, leaf := range resp.getLeafResult.Leaves { @@ -269,19 +244,18 @@ func SearchLogQueryHandler(params entries.SearchLogQueryParams) middleware.Respo } if len(params.Entry.LogIndexes) > 0 { - resp, err := api.client.getLeafByIndex(swag.Int64ValueSlice(params.Entry.LogIndexes)) - if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(entries.NewSearchLogQueryDefault(code), code, err, trillianCommunicationError, params.HTTPRequest) - } - switch resp.status { - case codes.OK, codes.NotFound: - default: - code := http.StatusInternalServerError - return logAndReturnError(entries.NewSearchLogQueryDefault(code), code, nil, trillianUnexpectedResult, params.HTTPRequest) + leaves := []*trillian.LogLeaf{} + for _, logIndex := range params.Entry.LogIndexes { + resp := api.client.getLeafByIndex(swag.Int64Value(logIndex)) + switch resp.status { + case codes.OK, codes.NotFound: + default: + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc error: %w", resp.err), trillianUnexpectedResult) + } + leaves = append(leaves, resp.getLeafResult.Leaves...) } - for _, leaf := range resp.getLeafResult.Leaves { + for _, leaf := range leaves { logEntry := models.LogEntry{ hex.EncodeToString(leaf.MerkleLeafHash): models.LogEntryAnon{ LogIndex: &leaf.LeafIndex, diff --git a/pkg/api/error.go b/pkg/api/error.go index e6e09f17fc113ce3304ec6fed412417140fda2f9..5121cbae7b3b9304e98a0ae68da270f1aa9b8242 100644 --- a/pkg/api/error.go +++ b/pkg/api/error.go @@ -16,11 +16,15 @@ limitations under the License. package api import ( + "fmt" "net/http" - "reflect" + "regexp" "github.com/go-openapi/runtime/middleware" + "github.com/mitchellh/mapstructure" "github.com/projectrekor/rekor/pkg/generated/models" + "github.com/projectrekor/rekor/pkg/generated/restapi/operations/entries" + "github.com/projectrekor/rekor/pkg/generated/restapi/operations/tlog" "github.com/projectrekor/rekor/pkg/log" ) @@ -30,22 +34,89 @@ const ( failedToGenerateCanonicalEntry = "Error generating canonicalized entry" entryAlreadyExists = "An equivalent entry already exists in the transparency log" firstSizeLessThanLastSize = "firstSize(%v) must be less than lastSize(%v)" + malformedUUID = "UUID must be a 64-character hexadecimal string" ) func errorMsg(message string, code int) *models.Error { - errObj := models.Error{ - Status: int64(code), + return &models.Error{ + Code: int64(code), Message: message, } - return &errObj } -func logAndReturnError(returnObj middleware.Responder, code int, err error, message string, r *http.Request) middleware.Responder { - log.RequestIDLogger(r).Errorf("returning %T(%v): message '%v', err '%v'", returnObj, code, message, err) - errorMsg := errorMsg(message, code) - if m, ok := reflect.TypeOf(returnObj).MethodByName("WithPayload"); ok { - args := []reflect.Value{reflect.ValueOf(returnObj), reflect.ValueOf(errorMsg)} - return m.Func.Call(args)[0].Interface().(middleware.Responder) +func handleRekorAPIError(params interface{}, code int, err error, message string, fields ...interface{}) middleware.Responder { + if message == "" { + message = http.StatusText(code) + } + + re := regexp.MustCompile("^(.*)Params$") + typeStr := fmt.Sprintf("%T", params) + handler := re.FindStringSubmatch(typeStr)[1] + + logMsg := func(r *http.Request) { + log.RequestIDLogger(r).Errorw("exiting with error", append([]interface{}{"handler", handler, "statusCode", code, "clientMessage", message, "error", err}, fields...)...) + paramsFields := map[string]interface{}{} + if err := mapstructure.Decode(params, ¶msFields); err == nil { + log.RequestIDLogger(r).Debug(paramsFields) + } + } + + switch params := params.(type) { + case entries.GetLogEntryByIndexParams: + logMsg(params.HTTPRequest) + switch code { + case http.StatusNotFound: + return entries.NewGetLogEntryByIndexNotFound() + default: + return entries.NewGetLogEntryByIndexDefault(code).WithPayload(errorMsg(message, code)) + } + case entries.GetLogEntryByUUIDParams: + logMsg(params.HTTPRequest) + switch code { + case http.StatusNotFound: + return entries.NewGetLogEntryByUUIDNotFound() + default: + return entries.NewGetLogEntryByUUIDDefault(code).WithPayload(errorMsg(message, code)) + } + case entries.GetLogEntryProofParams: + logMsg(params.HTTPRequest) + switch code { + case http.StatusNotFound: + return entries.NewGetLogEntryProofNotFound() + default: + return entries.NewGetLogEntryProofDefault(code).WithPayload(errorMsg(message, code)) + } + case entries.CreateLogEntryParams: + logMsg(params.HTTPRequest) + switch code { + case http.StatusBadRequest: + return entries.NewCreateLogEntryBadRequest() + case http.StatusConflict: + return entries.NewCreateLogEntryConflict() + default: + return entries.NewCreateLogEntryDefault(code).WithPayload(errorMsg(message, code)) + } + case entries.SearchLogQueryParams: + logMsg(params.HTTPRequest) + switch code { + case http.StatusBadRequest: + return entries.NewSearchLogQueryBadRequest() + default: + return entries.NewSearchLogQueryDefault(code).WithPayload(errorMsg(message, code)) + } + case tlog.GetLogInfoParams: + logMsg(params.HTTPRequest) + return tlog.NewGetLogInfoDefault(code).WithPayload(errorMsg(message, code)) + case tlog.GetLogProofParams: + logMsg(params.HTTPRequest) + switch code { + case http.StatusBadRequest: + return tlog.NewGetLogProofBadRequest() + default: + return tlog.NewGetLogProofDefault(code).WithPayload(errorMsg(message, code)) + } + default: + log.Logger.Errorf("unable to find method for type %T; error: %v", params, err) + return middleware.Error(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } - return returnObj } diff --git a/pkg/api/tlog.go b/pkg/api/tlog.go index 5eb4fb7d96ff3a35d9dddcf481a60d96ba3c91fb..c68a0b4fffcfc04d415ce4ce67469704573b9875 100644 --- a/pkg/api/tlog.go +++ b/pkg/api/tlog.go @@ -19,10 +19,12 @@ import ( "crypto" "crypto/x509" "encoding/hex" + "errors" "fmt" "net/http" "github.com/projectrekor/rekor/pkg/generated/models" + "google.golang.org/grpc/codes" "github.com/go-openapi/runtime/middleware" tclient "github.com/google/trillian/client" @@ -32,26 +34,24 @@ import ( ) func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder { - api, _ := NewAPI(params.HTTPRequest.Context()) + httpReq := params.HTTPRequest + api, _ := NewAPI(httpReq.Context()) - resp, err := api.client.getLatest(0) - if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(tlog.NewGetLogInfoDefault(code), code, err, trillianCommunicationError, params.HTTPRequest) + resp := api.client.getLatest(0) + if resp.status != codes.OK { + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc error: %w", resp.err), trillianCommunicationError) } result := resp.getLatestResult // validate result is signed with the key we're aware of pub, err := x509.ParsePKIXPublicKey(api.pubkey.Der) if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(tlog.NewGetLogInfoDefault(code), code, err, http.StatusText(code), params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, err, "") } verifier := tclient.NewLogVerifier(rfc6962.DefaultHasher, pub, crypto.SHA256) root, err := tcrypto.VerifySignedLogRoot(verifier.PubKey, verifier.SigHash, result.SignedLogRoot) if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(tlog.NewGetLogInfoDefault(code), code, err, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, err, trillianUnexpectedResult) } hashString := hex.EncodeToString(root.RootHash) @@ -65,29 +65,27 @@ func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder { } func GetLogProofHandler(params tlog.GetLogProofParams) middleware.Responder { + httpReq := params.HTTPRequest if *params.FirstSize > params.LastSize { - return logAndReturnError(tlog.NewGetLogProofBadRequest(), http.StatusBadRequest, nil, fmt.Sprintf(firstSizeLessThanLastSize, *params.FirstSize, params.LastSize), params.HTTPRequest) + return handleRekorAPIError(params, http.StatusBadRequest, nil, fmt.Sprintf(firstSizeLessThanLastSize, *params.FirstSize, params.LastSize)) } - api, _ := NewAPI(params.HTTPRequest.Context()) + api, _ := NewAPI(httpReq.Context()) - resp, err := api.client.getConsistencyProof(*params.FirstSize, params.LastSize) - if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(tlog.NewGetLogProofDefault(code), code, err, trillianCommunicationError, params.HTTPRequest) + resp := api.client.getConsistencyProof(*params.FirstSize, params.LastSize) + if resp.status != codes.OK { + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("grpc error: %w", resp.err), trillianCommunicationError) } result := resp.getConsistencyProofResult // validate result is signed with the key we're aware of pub, err := x509.ParsePKIXPublicKey(api.pubkey.Der) if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(tlog.NewGetLogProofDefault(code), code, err, http.StatusText(code), params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, err, "") } verifier := tclient.NewLogVerifier(rfc6962.DefaultHasher, pub, crypto.SHA256) root, err := tcrypto.VerifySignedLogRoot(verifier.PubKey, verifier.SigHash, result.SignedLogRoot) if err != nil { - code := http.StatusInternalServerError - return logAndReturnError(tlog.NewGetLogProofDefault(code), code, err, trillianUnexpectedResult, params.HTTPRequest) + return handleRekorAPIError(params, http.StatusInternalServerError, err, trillianUnexpectedResult) } hashString := hex.EncodeToString(root.RootHash) @@ -97,6 +95,8 @@ func GetLogProofHandler(params tlog.GetLogProofParams) middleware.Responder { for _, hash := range proof.Hashes { proofHashes = append(proofHashes, hex.EncodeToString(hash)) } + } else { + return handleRekorAPIError(params, http.StatusInternalServerError, errors.New("grpc call succeeded but no proof returned"), trillianUnexpectedResult) } consistencyProof := models.ConsistencyProof{ diff --git a/pkg/api/trillian_client.go b/pkg/api/trillian_client.go index 6763b4c25691b7acadf321a16f44556ad74520f8..283cc74826701f772dd49ee53cfab2c122f388c8 100644 --- a/pkg/api/trillian_client.go +++ b/pkg/api/trillian_client.go @@ -18,11 +18,9 @@ package api import ( "context" - "fmt" "time" "github.com/golang/protobuf/ptypes" - "github.com/projectrekor/rekor/pkg/log" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -36,24 +34,27 @@ import ( ) type TrillianClient struct { - client trillian.TrillianLogClient - logID int64 + client trillian.TrillianLogClient + logID int64 + context context.Context } type Response struct { status codes.Code + err error getAddResult *trillian.QueueLeafResponse getLeafResult *trillian.GetLeavesByHashResponse getProofResult *trillian.GetInclusionProofByHashResponse - getLeafByIndexResult *trillian.GetLeavesByIndexResponse + getLeafByRangeResult *trillian.GetLeavesByRangeResponse getLatestResult *trillian.GetLatestSignedLogRootResponse getConsistencyProofResult *trillian.GetConsistencyProofResponse } -func TrillianClientInstance(client trillian.TrillianLogClient, tLogID int64) *TrillianClient { +func TrillianClientInstance(client trillian.TrillianLogClient, tLogID int64, ctx context.Context) *TrillianClient { return &TrillianClient{ - client: client, - logID: tLogID, + client: client, + logID: tLogID, + context: ctx, } } @@ -61,7 +62,7 @@ func (t *TrillianClient) root() (types.LogRootV1, error) { rqst := &trillian.GetLatestSignedLogRootRequest{ LogId: t.logID, } - resp, err := t.client.GetLatestSignedLogRoot(context.Background(), rqst) + resp, err := t.client.GetLatestSignedLogRoot(t.context, rqst) if err != nil { return types.LogRootV1{}, err } @@ -72,7 +73,7 @@ func (t *TrillianClient) root() (types.LogRootV1, error) { return root, nil } -func (t *TrillianClient) addLeaf(byteValue []byte) (*Response, error) { +func (t *TrillianClient) addLeaf(byteValue []byte) *Response { leaf := &trillian.LogLeaf{ LeafValue: byteValue, } @@ -80,58 +81,59 @@ func (t *TrillianClient) addLeaf(byteValue []byte) (*Response, error) { LogId: t.logID, Leaf: leaf, } - resp, err := t.client.QueueLeaf(context.Background(), rqst) - if err != nil { - fmt.Println(err) - } + resp, err := t.client.QueueLeaf(t.context, rqst) return &Response{ - status: codes.Code(resp.QueuedLeaf.GetStatus().GetCode()), + status: status.Code(err), + err: err, getAddResult: resp, - }, nil + } } -func (t *TrillianClient) getLeafByHash(hashValues [][]byte) (*Response, error) { +func (t *TrillianClient) getLeafByHash(hashValues [][]byte) *Response { rqst := &trillian.GetLeavesByHashRequest{ LogId: t.logID, LeafHash: hashValues, } - resp, err := t.client.GetLeavesByHash(context.Background(), rqst) - if err != nil { - log.Logger.Fatal(err) - } + resp, err := t.client.GetLeavesByHash(t.context, rqst) return &Response{ status: status.Code(err), + err: err, getLeafResult: resp, - }, nil + } } -func (t *TrillianClient) getLeafByIndex(indexes []int64) (*Response, error) { +func (t *TrillianClient) getLeafByIndex(index int64) *Response { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + ctx, cancel := context.WithTimeout(t.context, 20*time.Second) defer cancel() - resp, err := t.client.GetLeavesByIndex(ctx, - &trillian.GetLeavesByIndexRequest{ - LogId: t.logID, - LeafIndex: indexes, + resp, err := t.client.GetLeavesByRange(ctx, + &trillian.GetLeavesByRangeRequest{ + LogId: t.logID, + StartIndex: index, + Count: 1, }) return &Response{ status: status.Code(err), - getLeafByIndexResult: resp, - }, nil + err: err, + getLeafByRangeResult: resp, + } } -func (t *TrillianClient) getProofByHash(hashValue []byte) (*Response, error) { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) +func (t *TrillianClient) getProofByHash(hashValue []byte) *Response { + ctx, cancel := context.WithTimeout(t.context, 20*time.Second) defer cancel() root, err := t.root() if err != nil { - return &Response{}, err + return &Response{ + status: status.Code(err), + err: err, + } } resp, err := t.client.GetInclusionProofByHash(ctx, @@ -144,26 +146,26 @@ func (t *TrillianClient) getProofByHash(hashValue []byte) (*Response, error) { v := merkle.NewLogVerifier(rfc6962.DefaultHasher) if resp != nil { - for i, proof := range resp.Proof { - hashes := proof.GetHashes() - for j, hash := range hashes { - log.Logger.Infof("Proof[%d],hash[%d] == %x\n", i, j, hash) - } - if err := v.VerifyInclusionProof(proof.LeafIndex, int64(root.TreeSize), hashes, root.RootHash, hashValue); err != nil { - return &Response{}, err + for _, proof := range resp.Proof { + if err := v.VerifyInclusionProof(proof.LeafIndex, int64(root.TreeSize), proof.GetHashes(), root.RootHash, hashValue); err != nil { + return &Response{ + status: status.Code(err), + err: err, + } } } } return &Response{ status: status.Code(err), + err: err, getProofResult: resp, - }, nil + } } -func (t *TrillianClient) getLatest(leafSizeInt int64) (*Response, error) { +func (t *TrillianClient) getLatest(leafSizeInt int64) *Response { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + ctx, cancel := context.WithTimeout(t.context, 20*time.Second) defer cancel() resp, err := t.client.GetLatestSignedLogRoot(ctx, @@ -171,19 +173,17 @@ func (t *TrillianClient) getLatest(leafSizeInt int64) (*Response, error) { LogId: t.logID, FirstTreeSize: leafSizeInt, }) - if err != nil { - return nil, err - } return &Response{ status: status.Code(err), + err: err, getLatestResult: resp, - }, nil + } } -func (t *TrillianClient) getConsistencyProof(firstSize, lastSize int64) (*Response, error) { +func (t *TrillianClient) getConsistencyProof(firstSize, lastSize int64) *Response { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + ctx, cancel := context.WithTimeout(t.context, 20*time.Second) defer cancel() resp, err := t.client.GetConsistencyProof(ctx, @@ -192,14 +192,12 @@ func (t *TrillianClient) getConsistencyProof(firstSize, lastSize int64) (*Respon FirstTreeSize: firstSize, SecondTreeSize: lastSize, }) - if err != nil { - return nil, err - } return &Response{ status: status.Code(err), + err: err, getConsistencyProofResult: resp, - }, nil + } } func createAndInitTree(ctx context.Context, adminClient trillian.TrillianAdminClient, logClient trillian.TrillianLogClient) (*trillian.Tree, error) { diff --git a/pkg/generated/models/error.go b/pkg/generated/models/error.go index 66d27a7931b792c1276f7abf0fb41dd2b201e46c..4f8232f98bc2d0bdd8b920625467739f8db8e468 100644 --- a/pkg/generated/models/error.go +++ b/pkg/generated/models/error.go @@ -34,11 +34,11 @@ import ( // swagger:model Error type Error struct { + // code + Code int64 `json:"code,omitempty"` + // message Message string `json:"message,omitempty"` - - // status - Status int64 `json:"status,omitempty"` } // Validate validates this error diff --git a/pkg/generated/models/search_log_query.go b/pkg/generated/models/search_log_query.go index d5b7da50d4346fe4b9a74dca299c2b1b12f064ec..7c27203a18ba5bdb2c4c529aa71bc9b6e829242c 100644 --- a/pkg/generated/models/search_log_query.go +++ b/pkg/generated/models/search_log_query.go @@ -140,6 +140,10 @@ func (m *SearchLogQuery) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateEntryUUIDs(formats); err != nil { + res = append(res, err) + } + if err := m.validateLogIndexes(formats); err != nil { res = append(res, err) } @@ -169,6 +173,22 @@ func (m *SearchLogQuery) validateEntries(formats strfmt.Registry) error { return nil } +func (m *SearchLogQuery) validateEntryUUIDs(formats strfmt.Registry) error { + if swag.IsZero(m.EntryUUIDs) { // not required + return nil + } + + for i := 0; i < len(m.EntryUUIDs); i++ { + + if err := validate.Pattern("entryUUIDs"+"."+strconv.Itoa(i), "body", string(m.EntryUUIDs[i]), `^[0-9a-fA-F]{64}$`); err != nil { + return err + } + + } + + return nil +} + func (m *SearchLogQuery) validateLogIndexes(formats strfmt.Registry) error { if swag.IsZero(m.LogIndexes) { // not required return nil diff --git a/pkg/generated/restapi/configure_rekor_server.go b/pkg/generated/restapi/configure_rekor_server.go index be1ef6e8f2f31938edc5deb6bf01c73c5ba95e2b..636712aac3c1a540abb3c6a4874e2972bfcfab9d 100644 --- a/pkg/generated/restapi/configure_rekor_server.go +++ b/pkg/generated/restapi/configure_rekor_server.go @@ -19,11 +19,13 @@ package restapi import ( "crypto/tls" + "fmt" "net/http" "github.com/go-chi/chi/middleware" "github.com/go-openapi/errors" "github.com/go-openapi/runtime" + "github.com/mitchellh/mapstructure" pkgapi "github.com/projectrekor/rekor/pkg/api" "github.com/projectrekor/rekor/pkg/generated/restapi/operations" @@ -31,6 +33,8 @@ import ( "github.com/projectrekor/rekor/pkg/generated/restapi/operations/tlog" "github.com/projectrekor/rekor/pkg/log" "github.com/projectrekor/rekor/pkg/util" + + "github.com/urfave/negroni" ) //go:generate swagger generate server --target ../../generated --name RekorServer --spec ../../../openapi.yaml --principal interface{} --exclude-main @@ -41,7 +45,7 @@ func configureFlags(api *operations.RekorServerAPI) { func configureAPI(api *operations.RekorServerAPI) http.Handler { // configure the api here - api.ServeError = errors.ServeError + api.ServeError = logAndServeError // Set your custom logger if needed. Default one is log.Printf // Expected interface func(string, ...interface{}) @@ -50,7 +54,7 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler { // api.Logger = log.Printf api.Logger = log.Logger.Infof - api.UseSwaggerUI() + // api.UseSwaggerUI() // To continue using redoc as your UI, uncomment the following line // api.UseRedoc() @@ -73,6 +77,15 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler { api.ServerShutdown = func() {} + //api object in context + api.AddMiddlewareFor("POST", "/api/v1/log/entries", addTrillianAPI) + api.AddMiddlewareFor("POST", "/api/v1/log/entries/retrieve", addTrillianAPI) + api.AddMiddlewareFor("GET", "/api/v1/log", addTrillianAPI) + api.AddMiddlewareFor("GET", "/api/v1/log/proof", addTrillianAPI) + api.AddMiddlewareFor("GET", "/api/v1/log/entries/{entryUUID}/proof", addTrillianAPI) + api.AddMiddlewareFor("GET", "/api/v1/log/entries", addTrillianAPI) + api.AddMiddlewareFor("GET", "/api/v1/log/entries/{entryUUID}", addTrillianAPI) + //not cacheable api.AddMiddlewareFor("GET", "/api/v1/log", middleware.NoCache) api.AddMiddlewareFor("GET", "/api/v1/log/proof", middleware.NoCache) @@ -122,7 +135,32 @@ func setupGlobalMiddleware(handler http.Handler) http.Handler { func cacheForever(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", "s-maxage=31536000, max-age=31536000, immutable") - handler.ServeHTTP(w, r) + ww := negroni.NewResponseWriter(w) + ww.Before(func(w negroni.ResponseWriter) { + if w.Status() >= 200 && w.Status() <= 299 { + w.Header().Set("Cache-Control", "s-maxage=31536000, max-age=31536000, immutable") + } + }) + handler.ServeHTTP(ww, r) + }) +} + +func addTrillianAPI(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + apiCtx, err := pkgapi.AddAPIToContext(r.Context()) + if err != nil { + logAndServeError(w, r, fmt.Errorf("error adding trillian API object to request context: %v", err)) + } else { + handler.ServeHTTP(w, r.WithContext(apiCtx)) + } }) } + +func logAndServeError(w http.ResponseWriter, r *http.Request, err error) { + log.RequestIDLogger(r).Error(err) + requestFields := map[string]interface{}{} + if err := mapstructure.Decode(r, &requestFields); err == nil { + log.RequestIDLogger(r).Debug(requestFields) + } + errors.ServeError(w, r, err) +} diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go index b25048d76b61ef09c05bf28aaf9633e1096a350d..bf0e81a376a20f62f5ce1c0c8eb41d374badb4ff 100644 --- a/pkg/generated/restapi/embedded_spec.go +++ b/pkg/generated/restapi/embedded_spec.go @@ -195,6 +195,7 @@ func init() { "operationId": "getLogEntryByUUID", "parameters": [ { + "pattern": "^[0-9a-fA-F]{64}$", "type": "string", "description": "the UUID of the entry to be retrieved from the log. The UUID is also the merkle tree hash of the entry.", "name": "entryUUID", @@ -228,6 +229,7 @@ func init() { "operationId": "getLogEntryProof", "parameters": [ { + "pattern": "^[0-9a-fA-F]{64}$", "type": "string", "description": "the UUID of the entry for which the inclusion proof information should be returned", "name": "entryUUID", @@ -320,11 +322,11 @@ func init() { "Error": { "type": "object", "properties": { + "code": { + "type": "integer" + }, "message": { "type": "string" - }, - "status": { - "type": "integer" } } }, @@ -425,6 +427,7 @@ func init() { "type": "array", "items": { "type": "string", + "pattern": "^[0-9a-fA-F]{64}$", "minItems": 1 } }, @@ -671,6 +674,7 @@ func init() { "operationId": "getLogEntryByUUID", "parameters": [ { + "pattern": "^[0-9a-fA-F]{64}$", "type": "string", "description": "the UUID of the entry to be retrieved from the log. The UUID is also the merkle tree hash of the entry.", "name": "entryUUID", @@ -707,6 +711,7 @@ func init() { "operationId": "getLogEntryProof", "parameters": [ { + "pattern": "^[0-9a-fA-F]{64}$", "type": "string", "description": "the UUID of the entry for which the inclusion proof information should be returned", "name": "entryUUID", @@ -808,11 +813,11 @@ func init() { "Error": { "type": "object", "properties": { + "code": { + "type": "integer" + }, "message": { "type": "string" - }, - "status": { - "type": "integer" } } }, @@ -1082,7 +1087,8 @@ func init() { "entryUUIDs": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^[0-9a-fA-F]{64}$" } }, "logIndexes": { diff --git a/pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_parameters.go b/pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_parameters.go index 7356743d64b00f125d79965f4c69ae34ebf2c7eb..908383ee56904fb82c5fd9ec4327a34cebbe950a 100644 --- a/pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_parameters.go +++ b/pkg/generated/restapi/operations/entries/get_log_entry_by_uuid_parameters.go @@ -28,6 +28,7 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" ) // NewGetLogEntryByUUIDParams creates a new GetLogEntryByUUIDParams object @@ -48,6 +49,7 @@ type GetLogEntryByUUIDParams struct { /*the UUID of the entry to be retrieved from the log. The UUID is also the merkle tree hash of the entry. Required: true + Pattern: ^[0-9a-fA-F]{64}$ In: path */ EntryUUID string @@ -85,5 +87,19 @@ func (o *GetLogEntryByUUIDParams) bindEntryUUID(rawData []string, hasKey bool, f o.EntryUUID = raw + if err := o.validateEntryUUID(formats); err != nil { + return err + } + + return nil +} + +// validateEntryUUID carries on validations for parameter EntryUUID +func (o *GetLogEntryByUUIDParams) validateEntryUUID(formats strfmt.Registry) error { + + if err := validate.Pattern("entryUUID", "path", o.EntryUUID, `^[0-9a-fA-F]{64}$`); err != nil { + return err + } + return nil } diff --git a/pkg/generated/restapi/operations/entries/get_log_entry_proof_parameters.go b/pkg/generated/restapi/operations/entries/get_log_entry_proof_parameters.go index fdb3902fdee72a599cb21851d175da6e861e45c1..2a3cee38c4f151608aa371f7eefb8664b9301d7c 100644 --- a/pkg/generated/restapi/operations/entries/get_log_entry_proof_parameters.go +++ b/pkg/generated/restapi/operations/entries/get_log_entry_proof_parameters.go @@ -28,6 +28,7 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" ) // NewGetLogEntryProofParams creates a new GetLogEntryProofParams object @@ -48,6 +49,7 @@ type GetLogEntryProofParams struct { /*the UUID of the entry for which the inclusion proof information should be returned Required: true + Pattern: ^[0-9a-fA-F]{64}$ In: path */ EntryUUID string @@ -85,5 +87,19 @@ func (o *GetLogEntryProofParams) bindEntryUUID(rawData []string, hasKey bool, fo o.EntryUUID = raw + if err := o.validateEntryUUID(formats); err != nil { + return err + } + + return nil +} + +// validateEntryUUID carries on validations for parameter EntryUUID +func (o *GetLogEntryProofParams) validateEntryUUID(formats strfmt.Registry) error { + + if err := validate.Pattern("entryUUID", "path", o.EntryUUID, `^[0-9a-fA-F]{64}$`); err != nil { + return err + } + return nil }