From 6ae68f109fcc4f0c1dcc65a55ad34904f93adf19 Mon Sep 17 00:00:00 2001 From: Bob Callaway <bobcallaway@users.noreply.github.com> Date: Mon, 7 Dec 2020 04:24:01 -0500 Subject: [PATCH] Types unit test (#57) * add types unit tests * explicitly ignore error from ValidateLeaf * improve PGP unit test coverage (#58) * update & improve pgp unit tests * add types unit tests * explicitly ignore error from ValidateLeaf * use files instead of embedded strings --- go.mod | 1 + go.sum | 8 + pkg/types/types.go | 31 ++- pkg/types/types_test.go | 418 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 452 insertions(+), 6 deletions(-) create mode 100644 pkg/types/types_test.go diff --git a/go.mod b/go.mod index 6e1245f..58eb0d7 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/prometheus/common v0.10.0 github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.7.1 + github.com/tidwall/sjson v1.1.2 go.etcd.io/etcd v3.3.25+incompatible // indirect go.uber.org/goleak v1.1.10 go.uber.org/zap v1.16.0 diff --git a/go.sum b/go.sum index 791cea8..8bf2d70 100644 --- a/go.sum +++ b/go.sum @@ -514,6 +514,14 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/gjson v1.6.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws= +github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0= +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= +github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/sjson v1.1.2 h1:NC5okI+tQ8OG/oyzchvwXXxRxCV/FVdhODbPKkQ25jQ= +github.com/tidwall/sjson v1.1.2/go.mod h1:SEzaDwxiPzKzNfUEO4HbYF/m4UCSJDsGgNqsS1LvdoY= github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= diff --git a/pkg/types/types.go b/pkg/types/types.go index fd13f89..d58e056 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -28,6 +28,8 @@ import ( "fmt" "io" "net/http" + "net/url" + "reflect" "github.com/projectrekor/rekor/pkg/pki" "golang.org/x/sync/errgroup" @@ -59,10 +61,17 @@ func (r *RekorLeaf) MarshalJSON() ([]byte, error) { cLeaf.SHA = r.SHA var err error + if reflect.ValueOf(r.sigObject).IsNil() { + return nil, errors.New("signature has not been initialized") + } cLeaf.Signature, err = r.sigObject.CanonicalValue() if err != nil { return nil, err } + + if reflect.ValueOf(r.keyObject).IsNil() { + return nil, errors.New("public key has not been initialized") + } cLeaf.PublicKey, err = r.keyObject.CanonicalValue() if err != nil { return nil, err @@ -87,7 +96,7 @@ func (l *RekorLeaf) ValidateLeaf() error { // validate fields if l.SHA != "" { if _, err := hex.DecodeString(l.SHA); err != nil || len(l.SHA) != 64 { - return fmt.Errorf("Invalid SHA hash provided") + return fmt.Errorf("invalid SHA hash provided") } } @@ -109,6 +118,10 @@ func (l *RekorLeaf) ValidateLeaf() error { } func ParseRekorEntry(r io.Reader, leaf RekorLeaf) (*RekorEntry, error) { + if err := leaf.ValidateLeaf(); err != nil { + return nil, err + } + var e RekorEntry dec := json.NewDecoder(r) if err := dec.Decode(&e); err != nil && err != io.EOF { @@ -119,11 +132,17 @@ func ParseRekorEntry(r io.Reader, leaf RekorLeaf) (*RekorEntry, error) { e.RekorLeaf = leaf if e.Data == nil && e.URL == "" { - return nil, errors.New("one of Contents or ContentsRef must be set") + return nil, errors.New("one of Data or URL must be set") } - if e.URL != "" && e.SHA == "" { - return nil, errors.New("SHA hash must be specified if URL is set") + if e.URL != "" { + if _, err := url.ParseRequestURI(e.URL); err != nil { + return nil, fmt.Errorf("url parsing failed: %w", err) + } + + if e.SHA == "" { + return nil, errors.New("SHA hash must be specified if URL is set") + } } return &e, nil @@ -143,7 +162,7 @@ func (r *RekorEntry) Load(ctx context.Context) error { return err } if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("Error received while fetching artifact: %v", resp.Status) + return fmt.Errorf("error received while fetching artifact: %v", resp.Status) } defer resp.Body.Close() @@ -154,7 +173,7 @@ func (r *RekorEntry) Load(ctx context.Context) error { return err } - if "application/x+gzip" == http.DetectContentType(ctBuf) { + if http.DetectContentType(ctBuf) == "application/x-gzip" { dataReader, _ = gzip.NewReader(io.MultiReader(bufReader, resp.Body)) } else { dataReader = io.MultiReader(bufReader, resp.Body) diff --git a/pkg/types/types_test.go b/pkg/types/types_test.go new file mode 100644 index 0000000..62722ea --- /dev/null +++ b/pkg/types/types_test.go @@ -0,0 +1,418 @@ +/* +Copyright © 2020 Bob Callaway <bcallawa@redhat.com> + +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 types + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + + "github.com/tidwall/sjson" + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +type BadReader struct { +} + +func (br BadReader) Read(p []byte) (n int, err error) { + return 0, errors.New("test error") +} + +func jsonSetNoError(input, path string, value interface{}) string { + ret, _ := sjson.Set(input, path, value) + return ret +} + +func jsonDeleteNoError(input, path string) string { + ret, _ := sjson.Delete(input, path) + return ret +} + +func TestRekorLeaf(t *testing.T) { + + type Test struct { + caseDesc string + json string + expectSuccess bool + } + + kernelJSONBytes, _ := ioutil.ReadFile("../../tests/kernel.json") + originalEntry := string(kernelJSONBytes) + originalLeaf := jsonDeleteNoError(originalEntry, "URL") + + tests := []Test{ + { + caseDesc: "Valid full entry", + json: originalEntry, + expectSuccess: true, + }, + { + caseDesc: "Valid full leaf", + json: originalLeaf, + expectSuccess: true, + }, + { + caseDesc: "Valid leaf without SHA", + json: jsonDeleteNoError(originalLeaf, "SHA"), + expectSuccess: true, + }, + { + caseDesc: "Invalid SHA", + json: jsonSetNoError(originalLeaf, "SHA", "not_a_valid_sha_hash!!!"), + expectSuccess: false, + }, + { + caseDesc: "Invalid signature", + json: jsonSetNoError(originalLeaf, "Signature", "not_a_signature!!!"), + expectSuccess: false, + }, + { + caseDesc: "Invalid public key", + json: jsonSetNoError(originalLeaf, "PublicKey", "c29tZV9kYXRhCg=="), + expectSuccess: false, + }, + { + caseDesc: "Missing signature", + json: jsonDeleteNoError(originalLeaf, "Signature"), + expectSuccess: false, + }, + { + caseDesc: "Missing public key", + json: jsonDeleteNoError(originalLeaf, "PublicKey"), + expectSuccess: false, + }, + { + caseDesc: "Invalid JSON", + json: `This is not valid JSON!`, + expectSuccess: false, + }, + { + caseDesc: "Empty JSON", + json: `{}`, + expectSuccess: false, + }, + } + + for _, tc := range tests { + leaf, err := ParseRekorLeaf(strings.NewReader(tc.json)) + if (err == nil) != tc.expectSuccess { + t.Errorf("Error in test case '%v': %v", tc.caseDesc, err) + } + + if tc.expectSuccess == true { + jsonMap := make(map[string]string) + err := json.Unmarshal([]byte(tc.json), &jsonMap) + if err != nil { + t.Errorf("Error in test case '%v' comparing leaf to input: %v", tc.caseDesc, err) + } + for key, val := range jsonMap { + if key == "SHA" { + if val != reflect.Indirect(reflect.ValueOf(leaf)).FieldByName(key).String() { + t.Errorf("Error in test case '%v': leaf does not reflect input for '%v'", tc.caseDesc, key) + } + } else if key == "Signature" || key == "PublicKey" { + leafVal := base64.StdEncoding.EncodeToString(reflect.Indirect(reflect.ValueOf(leaf)).FieldByName(key).Bytes()) + if val != leafVal { + t.Errorf("Error in test case '%v': leaf does not reflect input for '%v' %v != %v", tc.caseDesc, key, val, leafVal) + } + } + } + } + } + + if _, err := ParseRekorLeaf(&BadReader{}); err == nil { + t.Errorf("No error thrown if io.Reader failed") + } +} + +func TestRekorEntry(t *testing.T) { + type Test struct { + caseDesc string + json string + leaf *RekorLeaf + expectSuccess bool + } + + kernelJSONBytes, _ := ioutil.ReadFile("../../tests/kernel.json") + originalEntry := string(kernelJSONBytes) + + tests := []Test{ + { + caseDesc: "Valid full entry with URL & SHA", + json: originalEntry, + leaf: nil, + expectSuccess: true, + }, + { + caseDesc: "Valid full entry with Data", + json: jsonSetNoError(jsonDeleteNoError(jsonDeleteNoError(originalEntry, "URL"), "SHA"), "Data", "c29tZV9kYXRhCg=="), + leaf: nil, + expectSuccess: true, + }, + { + caseDesc: "Invalid entry without URL or Data", + json: jsonDeleteNoError(originalEntry, "URL"), + leaf: nil, + expectSuccess: false, + }, + { + caseDesc: "Invalid entry with URL but no SHA", + json: jsonDeleteNoError(originalEntry, "SHA"), + leaf: nil, + expectSuccess: false, + }, + { + caseDesc: "Invalid entry with bad URL", + json: jsonSetNoError(originalEntry, "URL", "not_a_url"), + leaf: nil, + expectSuccess: false, + }, + { + caseDesc: "Valid entry but empty leaf", + json: originalEntry, + leaf: &RekorLeaf{}, + expectSuccess: false, + }, + } + + for _, tc := range tests { + if tc.leaf == nil { + leaf, err := ParseRekorLeaf(strings.NewReader(tc.json)) + if err != nil { + t.Errorf("unexpected error in '%v': %v", tc.caseDesc, err) + } + tc.leaf = &leaf + } + + if _, err := ParseRekorEntry(strings.NewReader(tc.json), *(tc.leaf)); (err == nil) != tc.expectSuccess { + t.Errorf("Error in test case '%v': %v", tc.caseDesc, err) + } + } + + if _, err := ParseRekorEntry(&BadReader{}, RekorLeaf{}); err == nil { + t.Errorf("No error thrown if io.Reader failed") + } + + // valid leaf, invalid JSON + leaf, err := ParseRekorLeaf(strings.NewReader(tests[0].json)) + if err != nil { + t.Errorf("unexpected error in '%v': %v", tests[0].caseDesc, err) + } + + if _, err := ParseRekorEntry(strings.NewReader("not json"), leaf); err == nil { + t.Errorf("No error thrown for invalid JSON but valid leaf") + } +} + +func TestRekorLoad(t *testing.T) { + type Test struct { + caseDesc string + entry *RekorEntry + expectSuccess bool + } + + validLeafBytes, _ := ioutil.ReadFile("../../tests/kernel.json") + validLeaf, _ := ParseRekorLeaf(bytes.NewReader(validLeafBytes)) + + smallValidFileBytes, _ := ioutil.ReadFile("../../tests/test_file.txt") + smallValidLeafBytes, _ := ioutil.ReadFile("../../tests/rekor.json") + smallValidLeaf, _ := ParseRekorLeaf(bytes.NewReader(smallValidLeafBytes)) + + gzipValidFile := new(bytes.Buffer) + gzipW := gzip.NewWriter(gzipValidFile) + _, _ = gzipW.Write(smallValidFileBytes) + gzipW.Close() + + testServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path[1:] == "premature_close" { + return + } + if r.URL.Path[1:] == "not_found" { + w.WriteHeader(http.StatusNotFound) + return + } + if r.URL.Path[1:] == "invalidFile" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("doesn't match")) + } + if r.URL.Path[1:] == "smallValidFile" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(smallValidFileBytes) + } + if r.URL.Path[1:] == "gzipValidFile" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(gzipValidFile.Bytes()) + } + })) + defer testServer.Close() + + tests := []Test{ + { + caseDesc: "Invalid entry", + entry: &RekorEntry{}, + expectSuccess: false, + }, + { + caseDesc: "Invalid URL", + entry: &RekorEntry{ + nil, + "not_a_url", + RekorLeaf{validLeaf.SHA, validLeaf.Signature, validLeaf.PublicKey, nil, nil}, + }, + expectSuccess: false, + }, + { + caseDesc: "HTTP Server Closes Prematurely", + entry: &RekorEntry{ + nil, + testServer.URL + "/premature_close", + RekorLeaf{validLeaf.SHA, validLeaf.Signature, validLeaf.PublicKey, nil, nil}, + }, + expectSuccess: false, + }, + { + caseDesc: "HTTP Server 404 not found", + entry: &RekorEntry{ + nil, + testServer.URL + "/not_found", + RekorLeaf{validLeaf.SHA, validLeaf.Signature, validLeaf.PublicKey, nil, nil}, + }, + expectSuccess: false, + }, + { + caseDesc: "Valid small file (to test content_type detection lower limit", + entry: &RekorEntry{ + nil, + testServer.URL + "/smallValidFile", + RekorLeaf{smallValidLeaf.SHA, smallValidLeaf.Signature, smallValidLeaf.PublicKey, nil, nil}, + }, + expectSuccess: true, + }, + { + caseDesc: "Valid file with mismatched SHA value", + entry: &RekorEntry{ + nil, + testServer.URL + "/smallValidFile", + RekorLeaf{"1" + smallValidLeaf.SHA[1:], smallValidLeaf.Signature, smallValidLeaf.PublicKey, nil, nil}, + }, + expectSuccess: false, + }, + { + caseDesc: "Valid file with failed signature validation", + entry: &RekorEntry{ + nil, + testServer.URL + "/invalidFile", + RekorLeaf{validLeaf.SHA, validLeaf.Signature, validLeaf.PublicKey, nil, nil}, + }, + expectSuccess: false, + }, + { + caseDesc: "Valid gzipped file", + entry: &RekorEntry{ + nil, + testServer.URL + "/gzipValidFile", + RekorLeaf{smallValidLeaf.SHA, smallValidLeaf.Signature, smallValidLeaf.PublicKey, nil, nil}, + }, + expectSuccess: true, + }, + { + caseDesc: "Valid file passed in through Data field", + entry: &RekorEntry{ + smallValidFileBytes, + "", + RekorLeaf{"", smallValidLeaf.Signature, smallValidLeaf.PublicKey, nil, nil}, + }, + expectSuccess: true, + }, + } + + for _, tc := range tests { + ctx := context.TODO() + beforeSHA := tc.entry.SHA + if err := tc.entry.Load(ctx); (err == nil) != tc.expectSuccess { + t.Errorf("Error in test case '%v': %v", tc.caseDesc, err) + } + if tc.expectSuccess && beforeSHA == "" && tc.entry.SHA == "" { + t.Errorf("Error in test case '%v': SHA not set after call to Load()", tc.caseDesc) + } + } +} + +func TestRekorLeafMarshalJSON(t *testing.T) { + type Test struct { + caseDesc string + leaf *RekorLeaf + expectSuccess bool + } + + validLeafBytes, _ := ioutil.ReadFile("../../tests/kernel.json") + validLeaf, _ := ParseRekorLeaf(bytes.NewReader(validLeafBytes)) + + tests := []Test{ + { + caseDesc: "Valid Leaf", + leaf: &RekorLeaf{validLeaf.SHA, validLeaf.Signature, validLeaf.PublicKey, nil, nil}, + expectSuccess: true, + }, + { + caseDesc: "Invalid Leaf, bad sig", + leaf: &RekorLeaf{validLeaf.SHA, []byte("not a sig"), validLeaf.PublicKey, nil, nil}, + expectSuccess: false, + }, + { + caseDesc: "Invalid Leaf, bad key", + leaf: &RekorLeaf{validLeaf.SHA, validLeaf.Signature, []byte("not a key"), nil, nil}, + expectSuccess: false, + }, + { + caseDesc: "Invalid Leaf, empty", + leaf: &RekorLeaf{}, + expectSuccess: false, + }, + } + + for _, tc := range tests { + _ = tc.leaf.ValidateLeaf() + jsonOutput, err := json.Marshal(tc.leaf) + if (err == nil) != tc.expectSuccess { + t.Errorf("Error in test case '%v': %v", tc.caseDesc, err) + } + if tc.expectSuccess { + var marshalledLeaf RekorLeaf + if err := json.Unmarshal(jsonOutput, &marshalledLeaf); err != nil { + t.Errorf("Error in test case '%v': unable to unmarshal JSON returned", tc.caseDesc) + } + if tc.leaf.SHA != marshalledLeaf.SHA { + t.Errorf("Error in test case '%v': output SHA in JSON did not match input", tc.caseDesc) + } + } + } +} -- GitLab