From 7a23b4f2cf8447dcdf64ded1c2d64ca0960f3d71 Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Wed, 24 May 2023 16:10:41 -0700 Subject: [PATCH 01/11] wip Signed-off-by: Spencer Schrock --- .../models/verified_scorecard_result.go | 3 + app/generated/restapi/embedded_spec.go | 6 ++ app/server/post_results.go | 83 ++++++++++++------- app/server/post_results_e2e_test.go | 4 +- openapi.yaml | 2 + 5 files changed, 66 insertions(+), 32 deletions(-) diff --git a/app/generated/models/verified_scorecard_result.go b/app/generated/models/verified_scorecard_result.go index ff474258..2b75442e 100644 --- a/app/generated/models/verified_scorecard_result.go +++ b/app/generated/models/verified_scorecard_result.go @@ -40,6 +40,9 @@ type VerifiedScorecardResult struct { // result Result string `json:"result,omitempty"` + + // tlog index + TlogIndex int64 `json:"tlogIndex,omitempty"` } // Validate validates this verified scorecard result diff --git a/app/generated/restapi/embedded_spec.go b/app/generated/restapi/embedded_spec.go index efc574c7..a835c52f 100644 --- a/app/generated/restapi/embedded_spec.go +++ b/app/generated/restapi/embedded_spec.go @@ -325,6 +325,9 @@ func init() { }, "result": { "type": "string" + }, + "tlogIndex": { + "type": "integer" } } } @@ -674,6 +677,9 @@ func init() { }, "result": { "type": "string" + }, + "tlogIndex": { + "type": "integer" } } } diff --git a/app/server/post_results.go b/app/server/post_results.go index 9d485736..87654f23 100644 --- a/app/server/post_results.go +++ b/app/server/post_results.go @@ -52,6 +52,7 @@ const ( fulcioRepoSHAKey = "1.3.6.1.4.1.57264.1.3" resultsBucket = "gs://ossf-scorecard-results" resultsFile = "results.json" + noTlogIndex = 0 ) var ( @@ -62,6 +63,7 @@ var ( errCertMissingURI = errors.New("certificate has no URIs") errCertWorkflowPathEmpty = errors.New("cert workflow path is empty") errMismatchedCertAndRequest = errors.New("repository and branch of cert doesn't match that of request") + errNoTlogEntry = errors.New("no transparency log entry found") ) type certInfo struct { @@ -134,7 +136,7 @@ func PostResultsHandler(params results.PostResultParams) middleware.Responder { func processRequest(host, org, repo string, scorecardResult *models.VerifiedScorecardResult) error { ctx := context.Background() - cert, err := extractAndVerifyCertForPayload(ctx, []byte(scorecardResult.Result)) + cert, err := extractAndVerifyCertForPayload(ctx, []byte(scorecardResult.Result), scorecardResult.TlogIndex) if err != nil { return fmt.Errorf("error extracting cert: %w", err) } @@ -257,25 +259,36 @@ func writeToBlobStore(ctx context.Context, bucketURL, filename string, data []by return nil } -func extractAndVerifyCertForPayload(ctx context.Context, payload []byte) (*x509.Certificate, error) { - // Get most recent Rekor entry uuid. - uuids, err := getUUIDsByPayload(ctx, payload) - if err != nil || len(uuids) == 0 { - return nil, fmt.Errorf("error finding tlog entries corresponding to payload: %w", err) - } - // TODO(#135): We can't simply take the latest UUID. Either: - // (a) iterate through all returned UUIDs to find the right one. - // (b) send tlog index in the POST payload to identify the corresponding UUID. - uuid := uuids[len(uuids)-1] // ignore past entries. +func extractAndVerifyCertForPayload(ctx context.Context, payload []byte, tlogIndex int64) (*x509.Certificate, error) { + var entry *tlogEntry + var uuid string + var err error - // Get tlog entry from the UUID. - entry, err := getTLogEntry(ctx, uuid) - if err != nil { - return nil, fmt.Errorf("error fetching tlog entry: %w", err) + if tlogIndex == noTlogIndex { // older versions of scorecard action wont send the tlog index + // Get most recent Rekor entry uuid. + uuids, err := getUUIDsByPayload(ctx, payload) + if err != nil || len(uuids) == 0 { + return nil, fmt.Errorf("error finding tlog entries corresponding to payload: %w", err) + } + // TODO(#135): We can't simply take the latest UUID. Either: + // (a) iterate through all returned UUIDs to find the right one. + // (b) send tlog index in the POST payload to identify the corresponding UUID. + uuid = uuids[len(uuids)-1] // ignore past entries. + + // Get tlog entry from the UUID. + entry, err = getTLogEntryByUUID(ctx, uuid) + if err != nil { + return nil, fmt.Errorf("error fetching tlog entry: %w", err) + } + } else { + uuid, entry, err = getTLogEntryByIndex(ctx, tlogIndex) + if err != nil { + return nil, fmt.Errorf("error fetching tlog entry: %w", err) + } } // Verify inclusion proof. - if err := verifyInclusionProof(uuid, entry); err != nil { + if err = verifyInclusionProof(uuid, entry); err != nil { return nil, fmt.Errorf("verifying inclusion proof for Rekor: %w", err) } @@ -289,7 +302,7 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte) (*x509. } cert := certs[0] - if err := verifyCert(certs[0], time.Unix(entry.IntegratedTime, 0)); err != nil { + if err = verifyCert(certs[0], time.Unix(entry.IntegratedTime, 0)); err != nil { return nil, fmt.Errorf("verifying cert: %w", err) } return cert, nil @@ -334,32 +347,42 @@ func getUUIDsByPayload(ctx context.Context, payload []byte) ([]string, error) { return rekorResult, nil } -// getTLogEntry returns the tlog entry corresponding to the given UUID. -// It queries the Rekor server for the entry. -func getTLogEntry(ctx context.Context, uuid string) (*tlogEntry, error) { - rekorReq, err := http.NewRequestWithContext(ctx, - http.MethodGet, - fmt.Sprintf("https://rekor.sigstore.dev/api/v1/log/entries/%s", uuid), - nil) +// getTLogEntryHelper gets the uuid and tlog entry from Rekor using either a uuid or a tlog index. +func getTLogEntryHelper(ctx context.Context, url string) (uuid string, entry *tlogEntry, err error) { + rekorReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return nil, fmt.Errorf("creating new HTTP request: %w", err) + return "", nil, fmt.Errorf("creating new HTTP request: %w", err) } rekorReq.Header.Add("accept", "application/json") resp, err := http.DefaultClient.Do(rekorReq) if err != nil { - return nil, fmt.Errorf("looking up Rekor index: %w", err) + return "", nil, fmt.Errorf("looking up Rekor index: %w", err) } defer resp.Body.Close() var rekorResult map[string]tlogEntry if err := json.NewDecoder(resp.Body).Decode(&rekorResult); err != nil { - return nil, fmt.Errorf("decoding Rekor response: %w", err) + return "", nil, fmt.Errorf("decoding Rekor response: %w", err) } - for _, res := range rekorResult { - return &res, nil + for uuid, res := range rekorResult { + return uuid, &res, nil } - return nil, fmt.Errorf("unexpected error: entry for uuid %s not found", uuid) + return "", nil, errNoTlogEntry +} + +// getTLogEntryByIndex returns the UUID and tlog entry corresponding to the given Rekor tlog index. +func getTLogEntryByIndex(ctx context.Context, index int64) (uuid string, entry *tlogEntry, err error) { + url := fmt.Sprintf("https://rekor.sigstore.dev/api/v1/log/entries?logIndex=%d", index) + return getTLogEntryHelper(ctx, url) +} + +// getTLogEntryByUUID returns the tlog entry corresponding to the given UUID. +// It queries the Rekor server for the entry. +func getTLogEntryByUUID(ctx context.Context, uuid string) (*tlogEntry, error) { + url := fmt.Sprintf("https://rekor.sigstore.dev/api/v1/log/entries/%s", uuid) + _, entry, err := getTLogEntryHelper(ctx, url) + return entry, err } // verifyInclusionProof verifies the inclusion proof of the tlog entry. diff --git a/app/server/post_results_e2e_test.go b/app/server/post_results_e2e_test.go index 26574ba6..ffa4baa5 100644 --- a/app/server/post_results_e2e_test.go +++ b/app/server/post_results_e2e_test.go @@ -34,7 +34,7 @@ var _ = Describe("E2E Test: extractAndVerifyCertForPayload", func() { payload, err := io.ReadAll(testFile) Expect(err).Should(BeNil()) - _, errCertExtract := extractAndVerifyCertForPayload(context.Background(), payload) + _, errCertExtract := extractAndVerifyCertForPayload(context.Background(), payload, noTlogIndex) Expect(errCertExtract).Should(BeNil()) }) } @@ -68,7 +68,7 @@ var _ = Describe("E2E Test: getAndVerifyWorkflowContent", func() { Expect(err).Should(BeNil()) ctx := context.Background() - cert, errCertExtract := extractAndVerifyCertForPayload(ctx, payload) + cert, errCertExtract := extractAndVerifyCertForPayload(ctx, payload, noTlogIndex) Expect(errCertExtract).Should(BeNil()) info, errCertExtractInfo := extractCertInfo(cert) diff --git a/openapi.yaml b/openapi.yaml index 5ec6a416..0223644e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -235,6 +235,8 @@ definitions: type: string accessToken: type: string + tlogIndex: + type: integer responses: NotFound: From e96f4cc0fc56d478c4c3548e71152b06ec8d95ac Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Thu, 25 May 2023 14:07:24 -0700 Subject: [PATCH 02/11] wip. either verify hash or check the signature. Signed-off-by: Spencer Schrock --- app/server/post_results.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/app/server/post_results.go b/app/server/post_results.go index 87654f23..1b249ccb 100644 --- a/app/server/post_results.go +++ b/app/server/post_results.go @@ -280,11 +280,23 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte, tlogInd if err != nil { return nil, fmt.Errorf("error fetching tlog entry: %w", err) } + fmt.Println(uuid, "verified by contents") } else { uuid, entry, err = getTLogEntryByIndex(ctx, tlogIndex) if err != nil { return nil, fmt.Errorf("error fetching tlog entry: %w", err) } + fmt.Println(uuid, "verified by index", tlogIndex) + } + + // verify signature + hash, err := extractHash(entry) + if err != nil { + return nil, err + } + haveHash := fmt.Sprintf("%x", sha256.Sum256(payload)) + if haveHash != hash { + return nil, fmt.Errorf("checksum mismatch") } // Verify inclusion proof. @@ -595,3 +607,26 @@ func getCertPool(cert []byte) (*x509.CertPool, error) { } return pool, nil } + +// extractCerts extracts the certificates from the tlog entry. +// It base64 decodes the tlog Body and extracts the public key. +// It uses the public key to pem decode the certificates. +func extractHash(entry *tlogEntry) (string, error) { + b, err := base64.StdEncoding.DecodeString(entry.Body) + if err != nil { + return "", err + } + + var entryBody struct { + Spec struct { + Data struct { + Hash map[string]string `json:"hash"` + } `json:"data"` + } `json:"spec"` + } + if err := json.Unmarshal(b, &entryBody); err != nil { + return "", err + } + + return entryBody.Spec.Data.Hash["value"], nil +} From 91821bcb561b6a2fd843eadf664778ae5ebe52de Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Thu, 8 Jun 2023 16:12:03 -0700 Subject: [PATCH 03/11] centralize hashedrekord body decoding. Signed-off-by: Spencer Schrock --- app/server/post_results.go | 98 ++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/app/server/post_results.go b/app/server/post_results.go index 1b249ccb..5e6f4ce7 100644 --- a/app/server/post_results.go +++ b/app/server/post_results.go @@ -53,6 +53,7 @@ const ( resultsBucket = "gs://ossf-scorecard-results" resultsFile = "results.json" noTlogIndex = 0 + hashedRekordKind = "hashedrekord" ) var ( @@ -64,6 +65,8 @@ var ( errCertWorkflowPathEmpty = errors.New("cert workflow path is empty") errMismatchedCertAndRequest = errors.New("repository and branch of cert doesn't match that of request") errNoTlogEntry = errors.New("no transparency log entry found") + errNotRekordEntry = errors.New("not a rekord entry") + errMismatchedTlogEntry = errors.New("tlog entry does not match payload") ) type certInfo struct { @@ -90,6 +93,25 @@ type tlogEntry struct { } `json:"verification"` } +// https://github.com/sigstore/rekor/blob/f01f9cd2c55eaddba9be28624fea793a26ad28c4/pkg/types/hashedrekord/v0.0.1/hashedrekord_v0_0_1_schema.json +// +//nolint:lll +type hashedRekordBody struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Spec struct { + Data struct { + Hash map[string]string `json:"hash"` + } `json:"data"` + Signature struct { + Content string `json:"content"` + PublicKey struct { + Content string `json:"content"` + } `json:"publicKey"` + } `json:"signature"` + } +} + //go:embed fulcio_v1.crt.pem var fulcioRoot []byte @@ -289,14 +311,13 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte, tlogInd fmt.Println(uuid, "verified by index", tlogIndex) } - // verify signature - hash, err := extractHash(entry) + rekordBody, err := entry.rekord() if err != nil { return nil, err } - haveHash := fmt.Sprintf("%x", sha256.Sum256(payload)) - if haveHash != hash { - return nil, fmt.Errorf("checksum mismatch") + + if !rekordBody.matches(payload) { + return nil, errMismatchedTlogEntry } // Verify inclusion proof. @@ -305,7 +326,7 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte, tlogInd } // Extract and verify certificate. - certs, err := extractCerts(entry) + certs, err := rekordBody.certs() if err != nil || len(certs) == 0 { return nil, fmt.Errorf("error extracting certificate from entry: %w", err) } @@ -553,32 +574,12 @@ func extractCertInfo(cert *x509.Certificate) (certInfo, error) { return ret, nil } -// extractCerts extracts the certificates from the tlog entry. -// It base64 decodes the tlog Body and extracts the public key. +// extracts x509 certs from the hashedrekord tlog entry. // It uses the public key to pem decode the certificates. -func extractCerts(entry *tlogEntry) ([]*x509.Certificate, error) { - b, err := base64.StdEncoding.DecodeString(entry.Body) +func (hr hashedRekordBody) certs() ([]*x509.Certificate, error) { + publicKey, err := base64.StdEncoding.DecodeString(hr.Spec.Signature.PublicKey.Content) if err != nil { - return nil, err - } - - var entryBody struct { - Spec struct { - Signature struct { - PublicKey struct { - Content string `json:"content"` - } `json:"publicKey"` - } `json:"signature"` - } `json:"spec"` - } - if err := json.Unmarshal(b, &entryBody); err != nil { - return nil, err - } - - publicKeyB64 := entryBody.Spec.Signature.PublicKey.Content - publicKey, err := base64.StdEncoding.DecodeString(publicKeyB64) - if err != nil { - return nil, err + return nil, fmt.Errorf("decode rekord public key: %w", err) } remaining := publicKey @@ -599,6 +600,17 @@ func extractCerts(entry *tlogEntry) ([]*x509.Certificate, error) { return result, nil } +// check if the rekord object matches a given blob (currently uses sha256). +func (hr hashedRekordBody) matches(blob []byte) bool { + sha := sha256.Sum256(blob) + have := hex.EncodeToString(sha[:]) + want, ok := hr.Spec.Data.Hash["sha256"] + if !ok { + log.Println("hashed rekord entry has no sha256") + } + return have == want +} + func getCertPool(cert []byte) (*x509.CertPool, error) { pool := x509.NewCertPool() @@ -608,25 +620,17 @@ func getCertPool(cert []byte) (*x509.CertPool, error) { return pool, nil } -// extractCerts extracts the certificates from the tlog entry. -// It base64 decodes the tlog Body and extracts the public key. -// It uses the public key to pem decode the certificates. -func extractHash(entry *tlogEntry) (string, error) { - b, err := base64.StdEncoding.DecodeString(entry.Body) +func (t tlogEntry) rekord() (hashedRekordBody, error) { + b, err := base64.StdEncoding.DecodeString(t.Body) if err != nil { - return "", err + return hashedRekordBody{}, fmt.Errorf("decode rekord body: %w", err) } - - var entryBody struct { - Spec struct { - Data struct { - Hash map[string]string `json:"hash"` - } `json:"data"` - } `json:"spec"` + var body hashedRekordBody + if err := json.Unmarshal(b, &body); err != nil { + return hashedRekordBody{}, fmt.Errorf("unmarshal rekord body: %w", err) } - if err := json.Unmarshal(b, &entryBody); err != nil { - return "", err + if body.Kind != hashedRekordKind { + return hashedRekordBody{}, errNotRekordEntry } - - return entryBody.Spec.Data.Hash["value"], nil + return body, nil } From 92dc7d0b3c796347bec066ea073c9ea64a9fabe7 Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Thu, 8 Jun 2023 16:35:36 -0700 Subject: [PATCH 04/11] fix hashing match and fix comments/print statements. Signed-off-by: Spencer Schrock --- app/server/post_results.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/server/post_results.go b/app/server/post_results.go index f341052a..3244f3c2 100644 --- a/app/server/post_results.go +++ b/app/server/post_results.go @@ -290,13 +290,11 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte, tlogInd if err != nil { return nil, fmt.Errorf("error fetching tlog entry: %w", err) } - fmt.Println(uuid, "verified by contents") } else { uuid, entry, err = getTLogEntryByIndex(ctx, tlogIndex) if err != nil { return nil, fmt.Errorf("error fetching tlog entry: %w", err) } - fmt.Println(uuid, "verified by index", tlogIndex) } rekordBody, err := entry.rekord() @@ -368,8 +366,8 @@ func getUUIDsByPayload(ctx context.Context, payload []byte) ([]string, error) { return rekorResult, nil } -// getTLogEntryHelper gets the uuid and tlog entry from Rekor using either a uuid or a tlog index. -func getTLogEntryHelper(ctx context.Context, url string) (uuid string, entry *tlogEntry, err error) { +// tlog entry helper function, url should be a variation of the https://rekor.sigstore.dev/api/v1/log/entries endpoint. +func getTLogEntryFromURL(ctx context.Context, url string) (uuid string, entry *tlogEntry, err error) { rekorReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", nil, fmt.Errorf("creating new HTTP request: %w", err) @@ -392,17 +390,16 @@ func getTLogEntryHelper(ctx context.Context, url string) (uuid string, entry *tl return "", nil, errNoTlogEntry } -// getTLogEntryByIndex returns the UUID and tlog entry corresponding to the given Rekor tlog index. +// getTLogEntryByIndex fetches the UUID and tlog entry from Rekor by tlog index. func getTLogEntryByIndex(ctx context.Context, index int64) (uuid string, entry *tlogEntry, err error) { url := fmt.Sprintf("https://rekor.sigstore.dev/api/v1/log/entries?logIndex=%d", index) - return getTLogEntryHelper(ctx, url) + return getTLogEntryFromURL(ctx, url) } -// getTLogEntryByUUID returns the tlog entry corresponding to the given UUID. -// It queries the Rekor server for the entry. +// getTLogEntryByUUID fetches the tlog entry from Rekor by UUID. func getTLogEntryByUUID(ctx context.Context, uuid string) (*tlogEntry, error) { url := fmt.Sprintf("https://rekor.sigstore.dev/api/v1/log/entries/%s", uuid) - _, entry, err := getTLogEntryHelper(ctx, url) + _, entry, err := getTLogEntryFromURL(ctx, url) return entry, err } @@ -588,14 +585,15 @@ func (hr hashedRekordBody) certs() ([]*x509.Certificate, error) { return result, nil } -// check if the rekord object matches a given blob (currently uses sha256). +// check if the rekord object matches a given blob (currently compares sha256 hash). func (hr hashedRekordBody) matches(blob []byte) bool { - sha := sha256.Sum256(blob) - have := hex.EncodeToString(sha[:]) - want, ok := hr.Spec.Data.Hash["sha256"] - if !ok { + if hr.Spec.Data.Hash["algorithm"] != "sha256" { log.Println("hashed rekord entry has no sha256") + return false } + sha := sha256.Sum256(blob) + have := hex.EncodeToString(sha[:]) + want := hr.Spec.Data.Hash["value"] return have == want } From b6fd2398a46a53ace122747f71885ba044c39b65 Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Thu, 8 Jun 2023 17:29:59 -0700 Subject: [PATCH 05/11] update Signed-off-by: Spencer Schrock --- app/server/post_results.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/server/post_results.go b/app/server/post_results.go index 3244f3c2..f68c650f 100644 --- a/app/server/post_results.go +++ b/app/server/post_results.go @@ -274,15 +274,13 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte, tlogInd var uuid string var err error - if tlogIndex == noTlogIndex { // older versions of scorecard action wont send the tlog index + // #135 older versions of scorecard action wont send the tlog index, but newer ones will + if tlogIndex == noTlogIndex { // Get most recent Rekor entry uuid. uuids, err := getUUIDsByPayload(ctx, payload) if err != nil || len(uuids) == 0 { return nil, fmt.Errorf("error finding tlog entries corresponding to payload: %w", err) } - // TODO(#135): We can't simply take the latest UUID. Either: - // (a) iterate through all returned UUIDs to find the right one. - // (b) send tlog index in the POST payload to identify the corresponding UUID. uuid = uuids[len(uuids)-1] // ignore past entries. // Get tlog entry from the UUID. From 5e070df9d01fa99e986962fca810828b4ae6f8e2 Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Tue, 13 Jun 2023 11:17:32 -0700 Subject: [PATCH 06/11] Add tests for tlog and rekord funcs. Signed-off-by: Spencer Schrock --- app/server/post_results.go | 30 ++- app/server/post_results_test.go | 238 ++++++++++++++++++ .../testdata/rekor/log-entries-response.json | 38 +++ app/server/testdata/rekor/uploaded-blob.md | 1 + 4 files changed, 294 insertions(+), 13 deletions(-) create mode 100644 app/server/testdata/rekor/log-entries-response.json create mode 100644 app/server/testdata/rekor/uploaded-blob.md diff --git a/app/server/post_results.go b/app/server/post_results.go index f68c650f..f502d191 100644 --- a/app/server/post_results.go +++ b/app/server/post_results.go @@ -98,19 +98,23 @@ type tlogEntry struct { // //nolint:lll type hashedRekordBody struct { - APIVersion string `json:"apiVersion"` - Kind string `json:"kind"` - Spec struct { - Data struct { - Hash map[string]string `json:"hash"` - } `json:"data"` - Signature struct { - Content string `json:"content"` - PublicKey struct { - Content string `json:"content"` - } `json:"publicKey"` - } `json:"signature"` - } + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Spec hashedRekordSpec `json:"spec"` +} +type hashedRekordSpec struct { + Data hashedRekordData `json:"data"` + Signature hashedRekordSignature `json:"signature"` +} +type hashedRekordData struct { + Hash map[string]string +} +type hashedRekordSignature struct { + Content string `json:"content"` + PublicKey hashedrekordPublicKey `json:"publicKey"` +} +type hashedrekordPublicKey struct { + Content string `json:"content"` } //go:embed fulcio_v1.crt.pem diff --git a/app/server/post_results_test.go b/app/server/post_results_test.go index f15a8309..448363c4 100644 --- a/app/server/post_results_test.go +++ b/app/server/post_results_test.go @@ -15,15 +15,21 @@ package server import ( + "context" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "fmt" + "net/http" + "net/http/httptest" "net/url" + "os" "testing" "unicode/utf8" fuzz "github.com/AdaLogics/go-fuzz-headers" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" ) @@ -308,3 +314,235 @@ MYSKu39B6Q== }) } } + +func Test_getTLogEntryFromURL(t *testing.T) { + t.Parallel() + tests := []struct { + name string + responsePath string + uuid string + entry *tlogEntry + wantErr bool + }{ + { + name: "valid entry", + responsePath: "/rekor/log-entries-response.json", + uuid: "24296fb24b8ad77abaa457505061c4a0ef34197534bdd8b474acfafdc4d76c437726e98153c7b253", + entry: &tlogEntry{ + //nolint:lll + Body: "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjZDgzMjdkODY3ZmNlMDRiYzk3ZTE0OWRhNTBjMzc0NjM0MDg2OTU3NWY3YmY5NTlhNjcyODRlMzRiZmQ0NmJjIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURDM0hOdUtYOGttLzdUT28rSFExV0dyL0ZJVFJvWUQ5Znc1UkNScWM3V0lnSWhBTkFuNit5ejB6aUdHSEgvNTJwRG01dkN1T1FnSG5RMEFrR3FpNlBPa0VVQiIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXdWRU5EUVd4cFowRjNTVUpCWjBsVlpGaDRXSFJEYTJOQ2RtdDRVR2hJYzIweVR6ZHhjekppZG5Jd2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcE5kMDVxUlhwTlZHTjZUMVJSZVZkb1kwNU5hazEzVG1wRmVrMVVZekJQVkZGNVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZyVUV4c1ZraEdPRlJVUldNNVl6TXpibXhDV0hGRWVsTnFSM1JQWlVKb2EwVlhRMUFLTkV3NWMxaFlkVFF2TlhkbFl6TjNlVlk0TmtvemF6ZzVMeTlGVlRRdlN5c3JlRkk0Y2twa1pWRmlhbHBEVWtGUWFHRlBRMEZZWTNkblowWjZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZXVWsxTUNrbE9VbnBRWmpCVlRsRjBUMVJtVlZwTk1WTlVVVE5qZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsUldVUldVakJTUVZGSUwwSkNZM2RHV1VWVVl6Tk9hbUZJU25aWk1uUkJXakk1ZGxveWVHeE1iVTUyWWxSQmMwSm5iM0pDWjBWRlFWbFBMd3BOUVVWQ1FrSTFiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtSkhPVzVoVnpSMllqSkdNV1JIWjNkTVoxbExTM2RaUWtKQlIwUjJla0ZDQ2tOQlVXZEVRalZ2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2M1Ym1GWE5IWmlNa1l4WkVkbmQyZFpiMGREYVhOSFFWRlJRakZ1YTBNS1FrRkpSV1pCVWpaQlNHZEJaR2RFWkZCVVFuRjRjMk5TVFcxTldraG9lVnBhZW1ORGIydHdaWFZPTkRoeVppdElhVzVMUVV4NWJuVnFaMEZCUVZscE1Rb3hORE5VUVVGQlJVRjNRa2hOUlZWRFNVaDNlWGgxYkhGSk1tMUpkVTQ0YWk5NFRsY3JVbUp4YkdkMFMyWXpjVmx4TnpGalNEWTRNV1pGYUhOQmFVVkJDbWhsY1ZZM1NWUXpTek5rZUVReFR6TlhjRWxGY3poc1kxUmhTMVU0SzJvMVIyZzNUMUU0T1ZwT2FUQjNRMmRaU1V0dldrbDZhakJGUVhkTlJGcDNRWGNLV2tGSmQwSlRUMFIzWVZKMVExaG5ZMFZDUmsxS1FtaDRPVllyU1c5MlpIZE9PWHA1U1ZKNlZXbHZUbFpyVEhSUU0yODFTSFZ2TlZaamNFeE1jRUk1YndwU1VYTlNRV3BCVlM5eVJtRnJVMmRKYW14NGMyY3daV0pKUzI4ME5WSndkR3REU2toNmRVWldTM1l6VERkTVFpdExjemxSY25oV2IyZzRXbWhoTVZOaENqUk9lbWxsTmtVOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19", + IntegratedTime: 1686677983, + LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + LogIndex: 23652179, + }, + wantErr: false, + }, + } + ctx := context.Background() + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + url := setupServer(t) + tt.responsePath + uuid, entry, err := getTLogEntryFromURL(ctx, url) + if (err != nil) != tt.wantErr { + t.Fatalf("getTLogEntryFromURL() error = %v, wantErr %v", err, tt.wantErr) + } + if uuid != tt.uuid { + t.Errorf("getTLogEntryFromURL() uuid: %s, wanted %s", uuid, tt.uuid) + } + ignoreVerification := cmpopts.IgnoreFields(tlogEntry{}, "Verification") + if !cmp.Equal(entry, tt.entry, ignoreVerification) { + t.Error(cmp.Diff(entry, tt.entry, ignoreVerification)) + } + }) + } +} + +func setupServer(t *testing.T) string { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := os.ReadFile("./testdata" + r.URL.Path) + if err != nil { + t.Logf("os.ReadFile: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + w.Write(b) + })) + t.Cleanup(server.Close) + return server.URL +} + +func Test_tlogEntry_rekord(t *testing.T) { + t.Parallel() + tests := []struct { + name string + entry tlogEntry + want hashedRekordBody + wantErr bool + }{ + { + name: "basic", + entry: tlogEntry{ + //nolint:lll + Body: "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjZDgzMjdkODY3ZmNlMDRiYzk3ZTE0OWRhNTBjMzc0NjM0MDg2OTU3NWY3YmY5NTlhNjcyODRlMzRiZmQ0NmJjIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURDM0hOdUtYOGttLzdUT28rSFExV0dyL0ZJVFJvWUQ5Znc1UkNScWM3V0lnSWhBTkFuNit5ejB6aUdHSEgvNTJwRG01dkN1T1FnSG5RMEFrR3FpNlBPa0VVQiIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXdWRU5EUVd4cFowRjNTVUpCWjBsVlpGaDRXSFJEYTJOQ2RtdDRVR2hJYzIweVR6ZHhjekppZG5Jd2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcE5kMDVxUlhwTlZHTjZUMVJSZVZkb1kwNU5hazEzVG1wRmVrMVVZekJQVkZGNVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZyVUV4c1ZraEdPRlJVUldNNVl6TXpibXhDV0hGRWVsTnFSM1JQWlVKb2EwVlhRMUFLTkV3NWMxaFlkVFF2TlhkbFl6TjNlVlk0TmtvemF6ZzVMeTlGVlRRdlN5c3JlRkk0Y2twa1pWRmlhbHBEVWtGUWFHRlBRMEZZWTNkblowWjZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZXVWsxTUNrbE9VbnBRWmpCVlRsRjBUMVJtVlZwTk1WTlVVVE5qZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsUldVUldVakJTUVZGSUwwSkNZM2RHV1VWVVl6Tk9hbUZJU25aWk1uUkJXakk1ZGxveWVHeE1iVTUyWWxSQmMwSm5iM0pDWjBWRlFWbFBMd3BOUVVWQ1FrSTFiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtSkhPVzVoVnpSMllqSkdNV1JIWjNkTVoxbExTM2RaUWtKQlIwUjJla0ZDQ2tOQlVXZEVRalZ2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2M1Ym1GWE5IWmlNa1l4WkVkbmQyZFpiMGREYVhOSFFWRlJRakZ1YTBNS1FrRkpSV1pCVWpaQlNHZEJaR2RFWkZCVVFuRjRjMk5TVFcxTldraG9lVnBhZW1ORGIydHdaWFZPTkRoeVppdElhVzVMUVV4NWJuVnFaMEZCUVZscE1Rb3hORE5VUVVGQlJVRjNRa2hOUlZWRFNVaDNlWGgxYkhGSk1tMUpkVTQ0YWk5NFRsY3JVbUp4YkdkMFMyWXpjVmx4TnpGalNEWTRNV1pGYUhOQmFVVkJDbWhsY1ZZM1NWUXpTek5rZUVReFR6TlhjRWxGY3poc1kxUmhTMVU0SzJvMVIyZzNUMUU0T1ZwT2FUQjNRMmRaU1V0dldrbDZhakJGUVhkTlJGcDNRWGNLV2tGSmQwSlRUMFIzWVZKMVExaG5ZMFZDUmsxS1FtaDRPVllyU1c5MlpIZE9PWHA1U1ZKNlZXbHZUbFpyVEhSUU0yODFTSFZ2TlZaamNFeE1jRUk1YndwU1VYTlNRV3BCVlM5eVJtRnJVMmRKYW14NGMyY3daV0pKUzI4ME5WSndkR3REU2toNmRVWldTM1l6VERkTVFpdExjemxSY25oV2IyZzRXbWhoTVZOaENqUk9lbWxsTmtVOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19", + IntegratedTime: 1686677983, + LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + LogIndex: 23652179, + }, + want: hashedRekordBody{ + APIVersion: "0.0.1", + Kind: hashedRekordKind, + Spec: hashedRekordSpec{ + Data: hashedRekordData{ + Hash: map[string]string{ + "algorithm": "sha256", + "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", + }, + }, + Signature: hashedRekordSignature{ + Content: "MEYCIQDC3HNuKX8km/7TOo+HQ1WGr/FITRoYD9fw5RCRqc7WIgIhANAn6+yz0ziGGHH/52pDm5vCuOQgHnQ0AkGqi6POkEUB", + PublicKey: hashedrekordPublicKey{ + //nolint:lll + Content: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMwVENDQWxpZ0F3SUJBZ0lVZFh4WHRDa2NCdmt4UGhIc20yTzdxczJidnIwd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpNd05qRXpNVGN6T1RReVdoY05Nak13TmpFek1UYzBPVFF5V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVrUExsVkhGOFRURWM5YzMzbmxCWHFEelNqR3RPZUJoa0VXQ1AKNEw5c1hYdTQvNXdlYzN3eVY4Nkozazg5Ly9FVTQvSysreFI4ckpkZVFialpDUkFQaGFPQ0FYY3dnZ0Z6TUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVWUk1MCklOUnpQZjBVTlF0T1RmVVpNMVNUUTNjd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0lRWURWUjBSQVFIL0JCY3dGWUVUYzNOamFISnZZMnRBWjI5dloyeGxMbU52YlRBc0Jnb3JCZ0VFQVlPLwpNQUVCQkI1b2RIUndjem92TDJkcGRHaDFZaTVqYjIwdmJHOW5hVzR2YjJGMWRHZ3dMZ1lLS3dZQkJBR0R2ekFCCkNBUWdEQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHQVFRQjFua0MKQkFJRWZBUjZBSGdBZGdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnVqZ0FBQVlpMQoxNDNUQUFBRUF3QkhNRVVDSUh3eXh1bHFJMm1JdU44ai94TlcrUmJxbGd0S2YzcVlxNzFjSDY4MWZFaHNBaUVBCmhlcVY3SVQzSzNkeEQxTzNXcElFczhsY1RhS1U4K2o1R2g3T1E4OVpOaTB3Q2dZSUtvWkl6ajBFQXdNRFp3QXcKWkFJd0JTT0R3YVJ1Q1hnY0VCRk1KQmh4OVYrSW92ZHdOOXp5SVJ6VWlvTlZrTHRQM281SHVvNVZjcExMcEI5bwpSUXNSQWpBVS9yRmFrU2dJamx4c2cwZWJJS280NVJwdGtDSkh6dUZWS3YzTDdMQitLczlRcnhWb2g4WmhhMVNhCjROemllNkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r, err := tt.entry.rekord() + if (err != nil) != tt.wantErr { + t.Fatalf("(tlogEntry).rekord() error = %v, wantErr %v", err, tt.wantErr) + } + if !cmp.Equal(r, tt.want) { + t.Error(cmp.Diff(r, tt.want)) + } + }) + } +} + +func Test_rekord_certs(t *testing.T) { + t.Parallel() + tests := []struct { + name string + rekord hashedRekordBody + wantCount int + wantErr bool + }{ + { + name: "basic", + rekord: hashedRekordBody{ + APIVersion: "0.0.1", + Kind: hashedRekordKind, + Spec: hashedRekordSpec{ + Data: hashedRekordData{ + Hash: map[string]string{ + "algorithm": "sha256", + "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", + }, + }, + Signature: hashedRekordSignature{ + Content: "MEYCIQDC3HNuKX8km/7TOo+HQ1WGr/FITRoYD9fw5RCRqc7WIgIhANAn6+yz0ziGGHH/52pDm5vCuOQgHnQ0AkGqi6POkEUB", + PublicKey: hashedrekordPublicKey{ + //nolint:lll + Content: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMwVENDQWxpZ0F3SUJBZ0lVZFh4WHRDa2NCdmt4UGhIc20yTzdxczJidnIwd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpNd05qRXpNVGN6T1RReVdoY05Nak13TmpFek1UYzBPVFF5V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVrUExsVkhGOFRURWM5YzMzbmxCWHFEelNqR3RPZUJoa0VXQ1AKNEw5c1hYdTQvNXdlYzN3eVY4Nkozazg5Ly9FVTQvSysreFI4ckpkZVFialpDUkFQaGFPQ0FYY3dnZ0Z6TUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVWUk1MCklOUnpQZjBVTlF0T1RmVVpNMVNUUTNjd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0lRWURWUjBSQVFIL0JCY3dGWUVUYzNOamFISnZZMnRBWjI5dloyeGxMbU52YlRBc0Jnb3JCZ0VFQVlPLwpNQUVCQkI1b2RIUndjem92TDJkcGRHaDFZaTVqYjIwdmJHOW5hVzR2YjJGMWRHZ3dMZ1lLS3dZQkJBR0R2ekFCCkNBUWdEQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHQVFRQjFua0MKQkFJRWZBUjZBSGdBZGdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnVqZ0FBQVlpMQoxNDNUQUFBRUF3QkhNRVVDSUh3eXh1bHFJMm1JdU44ai94TlcrUmJxbGd0S2YzcVlxNzFjSDY4MWZFaHNBaUVBCmhlcVY3SVQzSzNkeEQxTzNXcElFczhsY1RhS1U4K2o1R2g3T1E4OVpOaTB3Q2dZSUtvWkl6ajBFQXdNRFp3QXcKWkFJd0JTT0R3YVJ1Q1hnY0VCRk1KQmh4OVYrSW92ZHdOOXp5SVJ6VWlvTlZrTHRQM281SHVvNVZjcExMcEI5bwpSUXNSQWpBVS9yRmFrU2dJamx4c2cwZWJJS280NVJwdGtDSkh6dUZWS3YzTDdMQitLczlRcnhWb2g4WmhhMVNhCjROemllNkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + }, + }, + }, + }, + wantCount: 1, + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + certs, err := tt.rekord.certs() + if (err != nil) != tt.wantErr { + t.Fatalf("(hashedRekordBody).certs() error = %v, wantErr %v", err, tt.wantErr) + } + if len(certs) != tt.wantCount { + t.Errorf("(hashedRekordBody).certs() parsed %d certs, wanted %d", len(certs), tt.wantCount) + } + }) + } +} + +func Test_rekord_matches(t *testing.T) { + t.Parallel() + tests := []struct { + name string + blobPath string + rekord hashedRekordBody + want bool + }{ + { + name: "valid match", + blobPath: "./testdata/rekor/uploaded-blob.md", + rekord: hashedRekordBody{ + Spec: hashedRekordSpec{ + Data: hashedRekordData{ + Hash: map[string]string{ + "algorithm": "sha256", + "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", + }, + }, + }, + }, + want: true, + }, + { + name: "not a match, mismatched hash", + blobPath: "./testdata/rekor/uploaded-blob.md", + rekord: hashedRekordBody{ + Spec: hashedRekordSpec{ + Data: hashedRekordData{ + Hash: map[string]string{ + "algorithm": "sha256", + "value": "foo", + }, + }, + }, + }, + want: false, + }, + { + name: "not sha256", + blobPath: "./testdata/rekor/uploaded-blob.md", + rekord: hashedRekordBody{ + Spec: hashedRekordSpec{ + Data: hashedRekordData{ + Hash: map[string]string{ + "algorithm": "foo", + "value": "bar", + }, + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + b, err := os.ReadFile(tt.blobPath) + if err != nil { + t.Fatalf("unable to read test file (%q): %v", tt.blobPath, err) + } + got := tt.rekord.matches(b) + if got != tt.want { + t.Errorf("(hashedRekordBody).matches() got %t, wanted %t", got, tt.want) + } + }) + } +} diff --git a/app/server/testdata/rekor/log-entries-response.json b/app/server/testdata/rekor/log-entries-response.json new file mode 100644 index 00000000..a98fa14d --- /dev/null +++ b/app/server/testdata/rekor/log-entries-response.json @@ -0,0 +1,38 @@ +{ + "24296fb24b8ad77abaa457505061c4a0ef34197534bdd8b474acfafdc4d76c437726e98153c7b253": { + "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjZDgzMjdkODY3ZmNlMDRiYzk3ZTE0OWRhNTBjMzc0NjM0MDg2OTU3NWY3YmY5NTlhNjcyODRlMzRiZmQ0NmJjIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURDM0hOdUtYOGttLzdUT28rSFExV0dyL0ZJVFJvWUQ5Znc1UkNScWM3V0lnSWhBTkFuNit5ejB6aUdHSEgvNTJwRG01dkN1T1FnSG5RMEFrR3FpNlBPa0VVQiIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXdWRU5EUVd4cFowRjNTVUpCWjBsVlpGaDRXSFJEYTJOQ2RtdDRVR2hJYzIweVR6ZHhjekppZG5Jd2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcE5kMDVxUlhwTlZHTjZUMVJSZVZkb1kwNU5hazEzVG1wRmVrMVVZekJQVkZGNVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZyVUV4c1ZraEdPRlJVUldNNVl6TXpibXhDV0hGRWVsTnFSM1JQWlVKb2EwVlhRMUFLTkV3NWMxaFlkVFF2TlhkbFl6TjNlVlk0TmtvemF6ZzVMeTlGVlRRdlN5c3JlRkk0Y2twa1pWRmlhbHBEVWtGUWFHRlBRMEZZWTNkblowWjZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZXVWsxTUNrbE9VbnBRWmpCVlRsRjBUMVJtVlZwTk1WTlVVVE5qZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBsUldVUldVakJTUVZGSUwwSkNZM2RHV1VWVVl6Tk9hbUZJU25aWk1uUkJXakk1ZGxveWVHeE1iVTUyWWxSQmMwSm5iM0pDWjBWRlFWbFBMd3BOUVVWQ1FrSTFiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtSkhPVzVoVnpSMllqSkdNV1JIWjNkTVoxbExTM2RaUWtKQlIwUjJla0ZDQ2tOQlVXZEVRalZ2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2M1Ym1GWE5IWmlNa1l4WkVkbmQyZFpiMGREYVhOSFFWRlJRakZ1YTBNS1FrRkpSV1pCVWpaQlNHZEJaR2RFWkZCVVFuRjRjMk5TVFcxTldraG9lVnBhZW1ORGIydHdaWFZPTkRoeVppdElhVzVMUVV4NWJuVnFaMEZCUVZscE1Rb3hORE5VUVVGQlJVRjNRa2hOUlZWRFNVaDNlWGgxYkhGSk1tMUpkVTQ0YWk5NFRsY3JVbUp4YkdkMFMyWXpjVmx4TnpGalNEWTRNV1pGYUhOQmFVVkJDbWhsY1ZZM1NWUXpTek5rZUVReFR6TlhjRWxGY3poc1kxUmhTMVU0SzJvMVIyZzNUMUU0T1ZwT2FUQjNRMmRaU1V0dldrbDZhakJGUVhkTlJGcDNRWGNLV2tGSmQwSlRUMFIzWVZKMVExaG5ZMFZDUmsxS1FtaDRPVllyU1c5MlpIZE9PWHA1U1ZKNlZXbHZUbFpyVEhSUU0yODFTSFZ2TlZaamNFeE1jRUk1YndwU1VYTlNRV3BCVlM5eVJtRnJVMmRKYW14NGMyY3daV0pKUzI4ME5WSndkR3REU2toNmRVWldTM1l6VERkTVFpdExjemxSY25oV2IyZzRXbWhoTVZOaENqUk9lbWxsTmtVOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19", + "integratedTime": 1686677983, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + "logIndex": 23652179, + "verification": { + "inclusionProof": { + "checkpoint": "rekor.sigstore.dev - 2605736670972794746\n19488771\n84/Lpa5FpT4LPld1d1pNw6zvHFK0Ek9h7uPTlngdhLc=\nTimestamp: 1686678005992195077\n\n— rekor.sigstore.dev wNI9ajBFAiA8YlKEMJZTnw//agkOBkgh5bx/oqnKhJneZfNSeaOCOwIhAMNxEc5afCZ60Skjq0ly4uQa7BxAtQMD7QJh1WMPJqDa\n", + "hashes": [ + "2cdde8b201dc09c029947b0d4d5e1d366b39c97118b3ad47c5d6c32619a425f4", + "d4e0e8963cd896205ecbc1abe98b052ba7235eac215d4592d474ff3637b107fd", + "7ef1679658a2a39b3671413e51818a6a13906b0768be6afdbfac066847d95fd5", + "7a60ab46bc3f5eca0bd9a4653c91812d65d966ec9fd2b0b55b7662d23fa5ad9c", + "8347eb55341891b7a48f828083d98011e175c2da55d4660be2082372c46060dd", + "91eb4ada7360f8f033641c3ed9750ca887da0719acf9b1faa4fc61c56f3ac062", + "b87daa55ce8a3ebf1431d95b96d68616f5bf6b09a60c1703d7cba1845486a992", + "975910b984c9d52b920723078744fb89b055d40b7cf52cb4921500cf5a4b859e", + "379bb64ca930affbc6c7b07cfb2b967b351005f70e3dc418841d29741b80b906", + "c8e25db102083f5acd39ca24a6fc343e7e0edac819ec68c695fccd1d1b20209f", + "69185c512ac79ad44454ba82262d0c158203cbcd7752f77bab1ff2b65a758a5e", + "e189c57f68547cdab5d2d3bd345feb079866667d64af0b6757653db285eef1f2", + "3925c001d70972896a6e397cce00099f709483764305d9c3d92022d6dcfd177d", + "e3f2a04d48f9c99f7368c18dd7e3dfb4a56360cb4db91cf1f439f7e2861843f5", + "ebf540dc5c863441e3186b7a7e5eeb4d213382564ed3e522fdc645529b10c9d6", + "84e6a1e7c2c30d133761ff69e9bf07779248950fac1c145cfbabb5a00ea33894", + "77d857e7affd618e58f409553993c9bd1726be7f26a376200a77a44d23e24490", + "0542b7c0d9441c96a6d760aea4efd40a833c60f0f9eef4c68ee2cd5ae3e0570b", + "ad712c98424de0f1284d4f144b8a95b5d22c181d4c0a246518e7a9a220bdf643" + ], + "logIndex": 19488748, + "rootHash": "f38fcba5ae45a53e0b3e5775775a4dc3acef1c52b4124f61eee3d396781d84b7", + "treeSize": 19488771 + }, + "signedEntryTimestamp": "MEUCIQCdLP8XglTGg+8UqKKdttt08/BP0b0197skwHdxsb29pgIgYlB1q2JNu/ocI3vmZWZF+p0UB2+2b5u4oTxtmuzBTC0=" + } + } +} diff --git a/app/server/testdata/rekor/uploaded-blob.md b/app/server/testdata/rekor/uploaded-blob.md new file mode 100644 index 00000000..ca72ea6e --- /dev/null +++ b/app/server/testdata/rekor/uploaded-blob.md @@ -0,0 +1 @@ +This is the file uploaded to Rekor's transparency log. From bb4540aa29f47080fff52cf6fb4fb09b397bdb04 Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Tue, 13 Jun 2023 11:45:31 -0700 Subject: [PATCH 07/11] move hashedrekord logic to separate package. Signed-off-by: Spencer Schrock --- .../internal/hashedrekord/hashedrekord.go | 89 +++++++++++ .../hashedrekord/hashedrekord_test.go | 138 ++++++++++++++++++ .../hashedrekord/testdata}/uploaded-blob.md | 0 app/server/post_results.go | 79 ++-------- app/server/post_results_test.go | 133 +---------------- 5 files changed, 244 insertions(+), 195 deletions(-) create mode 100644 app/server/internal/hashedrekord/hashedrekord.go create mode 100644 app/server/internal/hashedrekord/hashedrekord_test.go rename app/server/{testdata/rekor => internal/hashedrekord/testdata}/uploaded-blob.md (100%) diff --git a/app/server/internal/hashedrekord/hashedrekord.go b/app/server/internal/hashedrekord/hashedrekord.go new file mode 100644 index 00000000..e0da0696 --- /dev/null +++ b/app/server/internal/hashedrekord/hashedrekord.go @@ -0,0 +1,89 @@ +// Copyright 2023 OpenSSF Scorecard 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 hashedrekord + +import ( + "crypto/sha256" + "crypto/x509" + _ "embed" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "fmt" + "log" +) + +const Kind = "hashedrekord" + +// https://github.com/sigstore/rekor/blob/f01f9cd2c55eaddba9be28624fea793a26ad28c4/pkg/types/hashedrekord/v0.0.1/hashedrekord_v0_0_1_schema.json +// +//nolint:lll +type Body struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Spec Spec `json:"spec"` +} +type Spec struct { + Data Data `json:"data"` + Signature Signature `json:"signature"` +} +type Data struct { + Hash map[string]string +} +type Signature struct { + Content string `json:"content"` + PublicKey PublicKey `json:"publicKey"` +} +type PublicKey struct { + Content string `json:"content"` +} + +// check if the rekord object matches a given blob (currently compares sha256 hash). +func (b Body) Matches(blob []byte) bool { + if b.Spec.Data.Hash["algorithm"] != "sha256" { + log.Println("hashed rekord entry has no sha256") + return false + } + sha := sha256.Sum256(blob) + have := hex.EncodeToString(sha[:]) + want := b.Spec.Data.Hash["value"] + return have == want +} + +// extracts x509 certs from the hashedrekord tlog entry. +// It uses the public key to pem decode the certificates. +func (b Body) Certs() ([]*x509.Certificate, error) { + publicKey, err := base64.StdEncoding.DecodeString(b.Spec.Signature.PublicKey.Content) + if err != nil { + return nil, fmt.Errorf("decode rekord public key: %w", err) + } + + remaining := publicKey + var result []*x509.Certificate + for len(remaining) > 0 { + var certDer *pem.Block + certDer, remaining = pem.Decode(remaining) + if certDer == nil { + return nil, fmt.Errorf("error during PEM decoding: %w", err) + } + + cert, err := x509.ParseCertificate(certDer.Bytes) + if err != nil { + return nil, fmt.Errorf("error during certificate parsing: %w", err) + } + result = append(result, cert) + } + return result, nil +} diff --git a/app/server/internal/hashedrekord/hashedrekord_test.go b/app/server/internal/hashedrekord/hashedrekord_test.go new file mode 100644 index 00000000..0f37e8b9 --- /dev/null +++ b/app/server/internal/hashedrekord/hashedrekord_test.go @@ -0,0 +1,138 @@ +// Copyright 2023 OpenSSF Scorecard 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 hashedrekord + +import ( + "os" + "testing" +) + +func Test_Body_certs(t *testing.T) { + t.Parallel() + tests := []struct { + name string + body Body + wantCount int + wantErr bool + }{ + { + name: "basic", + body: Body{ + APIVersion: "0.0.1", + Kind: Kind, + Spec: Spec{ + Data: Data{ + Hash: map[string]string{ + "algorithm": "sha256", + "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", + }, + }, + Signature: Signature{ + Content: "MEYCIQDC3HNuKX8km/7TOo+HQ1WGr/FITRoYD9fw5RCRqc7WIgIhANAn6+yz0ziGGHH/52pDm5vCuOQgHnQ0AkGqi6POkEUB", + PublicKey: PublicKey{ + //nolint:lll + Content: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMwVENDQWxpZ0F3SUJBZ0lVZFh4WHRDa2NCdmt4UGhIc20yTzdxczJidnIwd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpNd05qRXpNVGN6T1RReVdoY05Nak13TmpFek1UYzBPVFF5V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVrUExsVkhGOFRURWM5YzMzbmxCWHFEelNqR3RPZUJoa0VXQ1AKNEw5c1hYdTQvNXdlYzN3eVY4Nkozazg5Ly9FVTQvSysreFI4ckpkZVFialpDUkFQaGFPQ0FYY3dnZ0Z6TUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVWUk1MCklOUnpQZjBVTlF0T1RmVVpNMVNUUTNjd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0lRWURWUjBSQVFIL0JCY3dGWUVUYzNOamFISnZZMnRBWjI5dloyeGxMbU52YlRBc0Jnb3JCZ0VFQVlPLwpNQUVCQkI1b2RIUndjem92TDJkcGRHaDFZaTVqYjIwdmJHOW5hVzR2YjJGMWRHZ3dMZ1lLS3dZQkJBR0R2ekFCCkNBUWdEQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHQVFRQjFua0MKQkFJRWZBUjZBSGdBZGdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnVqZ0FBQVlpMQoxNDNUQUFBRUF3QkhNRVVDSUh3eXh1bHFJMm1JdU44ai94TlcrUmJxbGd0S2YzcVlxNzFjSDY4MWZFaHNBaUVBCmhlcVY3SVQzSzNkeEQxTzNXcElFczhsY1RhS1U4K2o1R2g3T1E4OVpOaTB3Q2dZSUtvWkl6ajBFQXdNRFp3QXcKWkFJd0JTT0R3YVJ1Q1hnY0VCRk1KQmh4OVYrSW92ZHdOOXp5SVJ6VWlvTlZrTHRQM281SHVvNVZjcExMcEI5bwpSUXNSQWpBVS9yRmFrU2dJamx4c2cwZWJJS280NVJwdGtDSkh6dUZWS3YzTDdMQitLczlRcnhWb2g4WmhhMVNhCjROemllNkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + }, + }, + }, + }, + wantCount: 1, + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + certs, err := tt.body.Certs() + if (err != nil) != tt.wantErr { + t.Fatalf("(hashedRekordBody).certs() error = %v, wantErr %v", err, tt.wantErr) + } + if len(certs) != tt.wantCount { + t.Errorf("(hashedRekordBody).certs() parsed %d certs, wanted %d", len(certs), tt.wantCount) + } + }) + } +} + +func Test_Body_Matches(t *testing.T) { + t.Parallel() + tests := []struct { + name string + blobPath string + body Body + want bool + }{ + { + name: "valid match", + blobPath: "./testdata/uploaded-blob.md", + body: Body{ + Spec: Spec{ + Data: Data{ + Hash: map[string]string{ + "algorithm": "sha256", + "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", + }, + }, + }, + }, + want: true, + }, + { + name: "not a match, mismatched hash", + blobPath: "./testdata/uploaded-blob.md", + body: Body{ + Spec: Spec{ + Data: Data{ + Hash: map[string]string{ + "algorithm": "sha256", + "value": "foo", + }, + }, + }, + }, + want: false, + }, + { + name: "not sha256", + blobPath: "./testdata/uploaded-blob.md", + body: Body{ + Spec: Spec{ + Data: Data{ + Hash: map[string]string{ + "algorithm": "foo", + "value": "bar", + }, + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + b, err := os.ReadFile(tt.blobPath) + if err != nil { + t.Fatalf("unable to read test file (%q): %v", tt.blobPath, err) + } + got := tt.body.Matches(b) + if got != tt.want { + t.Errorf("(hashedRekordBody).matches() got %t, wanted %t", got, tt.want) + } + }) + } +} diff --git a/app/server/testdata/rekor/uploaded-blob.md b/app/server/internal/hashedrekord/testdata/uploaded-blob.md similarity index 100% rename from app/server/testdata/rekor/uploaded-blob.md rename to app/server/internal/hashedrekord/testdata/uploaded-blob.md diff --git a/app/server/post_results.go b/app/server/post_results.go index f502d191..aba9fcf8 100644 --- a/app/server/post_results.go +++ b/app/server/post_results.go @@ -42,6 +42,7 @@ import ( "github.com/ossf/scorecard-webapp/app/generated/models" "github.com/ossf/scorecard-webapp/app/generated/restapi/operations/results" + "github.com/ossf/scorecard-webapp/app/server/internal/hashedrekord" ) const ( @@ -53,7 +54,6 @@ const ( resultsBucket = "gs://ossf-scorecard-results" resultsFile = "results.json" noTlogIndex = 0 - hashedRekordKind = "hashedrekord" ) var ( @@ -94,29 +94,6 @@ type tlogEntry struct { } `json:"verification"` } -// https://github.com/sigstore/rekor/blob/f01f9cd2c55eaddba9be28624fea793a26ad28c4/pkg/types/hashedrekord/v0.0.1/hashedrekord_v0_0_1_schema.json -// -//nolint:lll -type hashedRekordBody struct { - APIVersion string `json:"apiVersion"` - Kind string `json:"kind"` - Spec hashedRekordSpec `json:"spec"` -} -type hashedRekordSpec struct { - Data hashedRekordData `json:"data"` - Signature hashedRekordSignature `json:"signature"` -} -type hashedRekordData struct { - Hash map[string]string -} -type hashedRekordSignature struct { - Content string `json:"content"` - PublicKey hashedrekordPublicKey `json:"publicKey"` -} -type hashedrekordPublicKey struct { - Content string `json:"content"` -} - //go:embed fulcio_v1.crt.pem var fulcioRoot []byte @@ -304,7 +281,7 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte, tlogInd return nil, err } - if !rekordBody.matches(payload) { + if !rekordBody.Matches(payload) { return nil, errMismatchedTlogEntry } @@ -314,7 +291,7 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte, tlogInd } // Extract and verify certificate. - certs, err := rekordBody.certs() + certs, err := rekordBody.Certs() if err != nil || len(certs) == 0 { return nil, fmt.Errorf("error extracting certificate from entry: %w", err) } @@ -561,44 +538,6 @@ func extractCertInfo(cert *x509.Certificate) (certInfo, error) { return ret, nil } -// extracts x509 certs from the hashedrekord tlog entry. -// It uses the public key to pem decode the certificates. -func (hr hashedRekordBody) certs() ([]*x509.Certificate, error) { - publicKey, err := base64.StdEncoding.DecodeString(hr.Spec.Signature.PublicKey.Content) - if err != nil { - return nil, fmt.Errorf("decode rekord public key: %w", err) - } - - remaining := publicKey - var result []*x509.Certificate - for len(remaining) > 0 { - var certDer *pem.Block - certDer, remaining = pem.Decode(remaining) - if certDer == nil { - return nil, fmt.Errorf("error during PEM decoding: %w", err) - } - - cert, err := x509.ParseCertificate(certDer.Bytes) - if err != nil { - return nil, fmt.Errorf("error during certificate parsing: %w", err) - } - result = append(result, cert) - } - return result, nil -} - -// check if the rekord object matches a given blob (currently compares sha256 hash). -func (hr hashedRekordBody) matches(blob []byte) bool { - if hr.Spec.Data.Hash["algorithm"] != "sha256" { - log.Println("hashed rekord entry has no sha256") - return false - } - sha := sha256.Sum256(blob) - have := hex.EncodeToString(sha[:]) - want := hr.Spec.Data.Hash["value"] - return have == want -} - func getCertPool(cert []byte) (*x509.CertPool, error) { pool := x509.NewCertPool() @@ -608,17 +547,17 @@ func getCertPool(cert []byte) (*x509.CertPool, error) { return pool, nil } -func (t tlogEntry) rekord() (hashedRekordBody, error) { +func (t tlogEntry) rekord() (hashedrekord.Body, error) { b, err := base64.StdEncoding.DecodeString(t.Body) if err != nil { - return hashedRekordBody{}, fmt.Errorf("decode rekord body: %w", err) + return hashedrekord.Body{}, fmt.Errorf("decode rekord body: %w", err) } - var body hashedRekordBody + var body hashedrekord.Body if err := json.Unmarshal(b, &body); err != nil { - return hashedRekordBody{}, fmt.Errorf("unmarshal rekord body: %w", err) + return hashedrekord.Body{}, fmt.Errorf("unmarshal rekord body: %w", err) } - if body.Kind != hashedRekordKind { - return hashedRekordBody{}, errNotRekordEntry + if body.Kind != hashedrekord.Kind { + return hashedrekord.Body{}, errNotRekordEntry } return body, nil } diff --git a/app/server/post_results_test.go b/app/server/post_results_test.go index 448363c4..0efedfaa 100644 --- a/app/server/post_results_test.go +++ b/app/server/post_results_test.go @@ -30,6 +30,7 @@ import ( fuzz "github.com/AdaLogics/go-fuzz-headers" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/ossf/scorecard-webapp/app/server/internal/hashedrekord" "github.com/stretchr/testify/assert" ) @@ -380,7 +381,7 @@ func Test_tlogEntry_rekord(t *testing.T) { tests := []struct { name string entry tlogEntry - want hashedRekordBody + want hashedrekord.Body wantErr bool }{ { @@ -392,19 +393,19 @@ func Test_tlogEntry_rekord(t *testing.T) { LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", LogIndex: 23652179, }, - want: hashedRekordBody{ + want: hashedrekord.Body{ APIVersion: "0.0.1", - Kind: hashedRekordKind, - Spec: hashedRekordSpec{ - Data: hashedRekordData{ + Kind: hashedrekord.Kind, + Spec: hashedrekord.Spec{ + Data: hashedrekord.Data{ Hash: map[string]string{ "algorithm": "sha256", "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", }, }, - Signature: hashedRekordSignature{ + Signature: hashedrekord.Signature{ Content: "MEYCIQDC3HNuKX8km/7TOo+HQ1WGr/FITRoYD9fw5RCRqc7WIgIhANAn6+yz0ziGGHH/52pDm5vCuOQgHnQ0AkGqi6POkEUB", - PublicKey: hashedrekordPublicKey{ + PublicKey: hashedrekord.PublicKey{ //nolint:lll Content: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMwVENDQWxpZ0F3SUJBZ0lVZFh4WHRDa2NCdmt4UGhIc20yTzdxczJidnIwd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpNd05qRXpNVGN6T1RReVdoY05Nak13TmpFek1UYzBPVFF5V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVrUExsVkhGOFRURWM5YzMzbmxCWHFEelNqR3RPZUJoa0VXQ1AKNEw5c1hYdTQvNXdlYzN3eVY4Nkozazg5Ly9FVTQvSysreFI4ckpkZVFialpDUkFQaGFPQ0FYY3dnZ0Z6TUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVWUk1MCklOUnpQZjBVTlF0T1RmVVpNMVNUUTNjd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0lRWURWUjBSQVFIL0JCY3dGWUVUYzNOamFISnZZMnRBWjI5dloyeGxMbU52YlRBc0Jnb3JCZ0VFQVlPLwpNQUVCQkI1b2RIUndjem92TDJkcGRHaDFZaTVqYjIwdmJHOW5hVzR2YjJGMWRHZ3dMZ1lLS3dZQkJBR0R2ekFCCkNBUWdEQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHQVFRQjFua0MKQkFJRWZBUjZBSGdBZGdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnVqZ0FBQVlpMQoxNDNUQUFBRUF3QkhNRVVDSUh3eXh1bHFJMm1JdU44ai94TlcrUmJxbGd0S2YzcVlxNzFjSDY4MWZFaHNBaUVBCmhlcVY3SVQzSzNkeEQxTzNXcElFczhsY1RhS1U4K2o1R2g3T1E4OVpOaTB3Q2dZSUtvWkl6ajBFQXdNRFp3QXcKWkFJd0JTT0R3YVJ1Q1hnY0VCRk1KQmh4OVYrSW92ZHdOOXp5SVJ6VWlvTlZrTHRQM281SHVvNVZjcExMcEI5bwpSUXNSQWpBVS9yRmFrU2dJamx4c2cwZWJJS280NVJwdGtDSkh6dUZWS3YzTDdMQitLczlRcnhWb2g4WmhhMVNhCjROemllNkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", }, @@ -428,121 +429,3 @@ func Test_tlogEntry_rekord(t *testing.T) { }) } } - -func Test_rekord_certs(t *testing.T) { - t.Parallel() - tests := []struct { - name string - rekord hashedRekordBody - wantCount int - wantErr bool - }{ - { - name: "basic", - rekord: hashedRekordBody{ - APIVersion: "0.0.1", - Kind: hashedRekordKind, - Spec: hashedRekordSpec{ - Data: hashedRekordData{ - Hash: map[string]string{ - "algorithm": "sha256", - "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", - }, - }, - Signature: hashedRekordSignature{ - Content: "MEYCIQDC3HNuKX8km/7TOo+HQ1WGr/FITRoYD9fw5RCRqc7WIgIhANAn6+yz0ziGGHH/52pDm5vCuOQgHnQ0AkGqi6POkEUB", - PublicKey: hashedrekordPublicKey{ - //nolint:lll - Content: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMwVENDQWxpZ0F3SUJBZ0lVZFh4WHRDa2NCdmt4UGhIc20yTzdxczJidnIwd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpNd05qRXpNVGN6T1RReVdoY05Nak13TmpFek1UYzBPVFF5V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVrUExsVkhGOFRURWM5YzMzbmxCWHFEelNqR3RPZUJoa0VXQ1AKNEw5c1hYdTQvNXdlYzN3eVY4Nkozazg5Ly9FVTQvSysreFI4ckpkZVFialpDUkFQaGFPQ0FYY3dnZ0Z6TUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVWUk1MCklOUnpQZjBVTlF0T1RmVVpNMVNUUTNjd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0lRWURWUjBSQVFIL0JCY3dGWUVUYzNOamFISnZZMnRBWjI5dloyeGxMbU52YlRBc0Jnb3JCZ0VFQVlPLwpNQUVCQkI1b2RIUndjem92TDJkcGRHaDFZaTVqYjIwdmJHOW5hVzR2YjJGMWRHZ3dMZ1lLS3dZQkJBR0R2ekFCCkNBUWdEQjVvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2Ykc5bmFXNHZiMkYxZEdnd2dZb0dDaXNHQVFRQjFua0MKQkFJRWZBUjZBSGdBZGdEZFBUQnF4c2NSTW1NWkhoeVpaemNDb2twZXVONDhyZitIaW5LQUx5bnVqZ0FBQVlpMQoxNDNUQUFBRUF3QkhNRVVDSUh3eXh1bHFJMm1JdU44ai94TlcrUmJxbGd0S2YzcVlxNzFjSDY4MWZFaHNBaUVBCmhlcVY3SVQzSzNkeEQxTzNXcElFczhsY1RhS1U4K2o1R2g3T1E4OVpOaTB3Q2dZSUtvWkl6ajBFQXdNRFp3QXcKWkFJd0JTT0R3YVJ1Q1hnY0VCRk1KQmh4OVYrSW92ZHdOOXp5SVJ6VWlvTlZrTHRQM281SHVvNVZjcExMcEI5bwpSUXNSQWpBVS9yRmFrU2dJamx4c2cwZWJJS280NVJwdGtDSkh6dUZWS3YzTDdMQitLczlRcnhWb2g4WmhhMVNhCjROemllNkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", - }, - }, - }, - }, - wantCount: 1, - wantErr: false, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - certs, err := tt.rekord.certs() - if (err != nil) != tt.wantErr { - t.Fatalf("(hashedRekordBody).certs() error = %v, wantErr %v", err, tt.wantErr) - } - if len(certs) != tt.wantCount { - t.Errorf("(hashedRekordBody).certs() parsed %d certs, wanted %d", len(certs), tt.wantCount) - } - }) - } -} - -func Test_rekord_matches(t *testing.T) { - t.Parallel() - tests := []struct { - name string - blobPath string - rekord hashedRekordBody - want bool - }{ - { - name: "valid match", - blobPath: "./testdata/rekor/uploaded-blob.md", - rekord: hashedRekordBody{ - Spec: hashedRekordSpec{ - Data: hashedRekordData{ - Hash: map[string]string{ - "algorithm": "sha256", - "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", - }, - }, - }, - }, - want: true, - }, - { - name: "not a match, mismatched hash", - blobPath: "./testdata/rekor/uploaded-blob.md", - rekord: hashedRekordBody{ - Spec: hashedRekordSpec{ - Data: hashedRekordData{ - Hash: map[string]string{ - "algorithm": "sha256", - "value": "foo", - }, - }, - }, - }, - want: false, - }, - { - name: "not sha256", - blobPath: "./testdata/rekor/uploaded-blob.md", - rekord: hashedRekordBody{ - Spec: hashedRekordSpec{ - Data: hashedRekordData{ - Hash: map[string]string{ - "algorithm": "foo", - "value": "bar", - }, - }, - }, - }, - want: false, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - b, err := os.ReadFile(tt.blobPath) - if err != nil { - t.Fatalf("unable to read test file (%q): %v", tt.blobPath, err) - } - got := tt.rekord.matches(b) - if got != tt.want { - t.Errorf("(hashedRekordBody).matches() got %t, wanted %t", got, tt.want) - } - }) - } -} From 8afe7df955f5994a6910f9ec8c40e29e0a0e085b Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Tue, 13 Jun 2023 11:55:49 -0700 Subject: [PATCH 08/11] fix linter Signed-off-by: Spencer Schrock --- app/server/post_results_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/server/post_results_test.go b/app/server/post_results_test.go index 0efedfaa..7985ac75 100644 --- a/app/server/post_results_test.go +++ b/app/server/post_results_test.go @@ -30,8 +30,9 @@ import ( fuzz "github.com/AdaLogics/go-fuzz-headers" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/ossf/scorecard-webapp/app/server/internal/hashedrekord" "github.com/stretchr/testify/assert" + + "github.com/ossf/scorecard-webapp/app/server/internal/hashedrekord" ) func Test_extractCertInfo(t *testing.T) { From e33b188d79f5fa5d3615bb61cda44883590b8e5c Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Tue, 13 Jun 2023 17:54:34 -0700 Subject: [PATCH 09/11] remove accidental blank import. Signed-off-by: Spencer Schrock --- app/server/internal/hashedrekord/hashedrekord.go | 1 - 1 file changed, 1 deletion(-) diff --git a/app/server/internal/hashedrekord/hashedrekord.go b/app/server/internal/hashedrekord/hashedrekord.go index e0da0696..83a56c47 100644 --- a/app/server/internal/hashedrekord/hashedrekord.go +++ b/app/server/internal/hashedrekord/hashedrekord.go @@ -17,7 +17,6 @@ package hashedrekord import ( "crypto/sha256" "crypto/x509" - _ "embed" "encoding/base64" "encoding/hex" "encoding/pem" From bca41e5ec6caaefd4356930ddb55886edd5266c0 Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Tue, 20 Jun 2023 12:37:20 -0700 Subject: [PATCH 10/11] Address feedback. Signed-off-by: Spencer Schrock --- .../internal/hashedrekord/hashedrekord.go | 16 ++++++++----- .../hashedrekord/hashedrekord_test.go | 24 +++++++++---------- app/server/post_results.go | 2 +- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/server/internal/hashedrekord/hashedrekord.go b/app/server/internal/hashedrekord/hashedrekord.go index 83a56c47..083231bb 100644 --- a/app/server/internal/hashedrekord/hashedrekord.go +++ b/app/server/internal/hashedrekord/hashedrekord.go @@ -24,6 +24,7 @@ import ( "log" ) +// https://github.com/sigstore/rekor/blob/f01f9cd2c55eaddba9be28624fea793a26ad28c4/pkg/types/README.md const Kind = "hashedrekord" // https://github.com/sigstore/rekor/blob/f01f9cd2c55eaddba9be28624fea793a26ad28c4/pkg/types/hashedrekord/v0.0.1/hashedrekord_v0_0_1_schema.json @@ -39,7 +40,11 @@ type Spec struct { Signature Signature `json:"signature"` } type Data struct { - Hash map[string]string + Hash Hash `json:"hash"` +} +type Hash struct { + Algorithm string `json:"algorithm"` + Value string `json:"value"` } type Signature struct { Content string `json:"content"` @@ -51,18 +56,17 @@ type PublicKey struct { // check if the rekord object matches a given blob (currently compares sha256 hash). func (b Body) Matches(blob []byte) bool { - if b.Spec.Data.Hash["algorithm"] != "sha256" { - log.Println("hashed rekord entry has no sha256") + if b.Spec.Data.Hash.Algorithm != "sha256" { + log.Println("hashedrekord entry has no sha256") return false } sha := sha256.Sum256(blob) have := hex.EncodeToString(sha[:]) - want := b.Spec.Data.Hash["value"] + want := b.Spec.Data.Hash.Value return have == want } -// extracts x509 certs from the hashedrekord tlog entry. -// It uses the public key to pem decode the certificates. +// extracts all x509 certs from the hashedrekord tlog entry public key. func (b Body) Certs() ([]*x509.Certificate, error) { publicKey, err := base64.StdEncoding.DecodeString(b.Spec.Signature.PublicKey.Content) if err != nil { diff --git a/app/server/internal/hashedrekord/hashedrekord_test.go b/app/server/internal/hashedrekord/hashedrekord_test.go index 0f37e8b9..1b351ba4 100644 --- a/app/server/internal/hashedrekord/hashedrekord_test.go +++ b/app/server/internal/hashedrekord/hashedrekord_test.go @@ -34,9 +34,9 @@ func Test_Body_certs(t *testing.T) { Kind: Kind, Spec: Spec{ Data: Data{ - Hash: map[string]string{ - "algorithm": "sha256", - "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", + Hash: Hash{ + Algorithm: "sha256", + Value: "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", }, }, Signature: Signature{ @@ -81,9 +81,9 @@ func Test_Body_Matches(t *testing.T) { body: Body{ Spec: Spec{ Data: Data{ - Hash: map[string]string{ - "algorithm": "sha256", - "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", + Hash: Hash{ + Algorithm: "sha256", + Value: "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", }, }, }, @@ -96,9 +96,9 @@ func Test_Body_Matches(t *testing.T) { body: Body{ Spec: Spec{ Data: Data{ - Hash: map[string]string{ - "algorithm": "sha256", - "value": "foo", + Hash: Hash{ + Algorithm: "sha256", + Value: "foo", }, }, }, @@ -111,9 +111,9 @@ func Test_Body_Matches(t *testing.T) { body: Body{ Spec: Spec{ Data: Data{ - Hash: map[string]string{ - "algorithm": "foo", - "value": "bar", + Hash: Hash{ + Algorithm: "foo", + Value: "bar", }, }, }, diff --git a/app/server/post_results.go b/app/server/post_results.go index aba9fcf8..5fc3e1ec 100644 --- a/app/server/post_results.go +++ b/app/server/post_results.go @@ -287,7 +287,7 @@ func extractAndVerifyCertForPayload(ctx context.Context, payload []byte, tlogInd // Verify inclusion proof. if err = verifyInclusionProof(uuid, entry); err != nil { - return nil, fmt.Errorf("verifying inclusion proof for Rekor: %w", err) + return nil, fmt.Errorf("unable to verify rekor inclusion proof: %w", err) } // Extract and verify certificate. From dc19b96a5d7de081aa41482bb99f7a0a9d76438a Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Tue, 20 Jun 2023 12:41:22 -0700 Subject: [PATCH 11/11] fix type mismatch from last commit. Signed-off-by: Spencer Schrock --- app/server/post_results_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/server/post_results_test.go b/app/server/post_results_test.go index 7985ac75..a5718619 100644 --- a/app/server/post_results_test.go +++ b/app/server/post_results_test.go @@ -399,9 +399,9 @@ func Test_tlogEntry_rekord(t *testing.T) { Kind: hashedrekord.Kind, Spec: hashedrekord.Spec{ Data: hashedrekord.Data{ - Hash: map[string]string{ - "algorithm": "sha256", - "value": "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", + Hash: hashedrekord.Hash{ + Algorithm: "sha256", + Value: "cd8327d867fce04bc97e149da50c3746340869575f7bf959a67284e34bfd46bc", }, }, Signature: hashedrekord.Signature{