Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT: ✨ Signed-Release: Add check for sigstore signatures #1846

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions checker/raw_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

package checker

import "time"
import (
"time"

"github.com/sigstore/rekor/pkg/generated/models"
)

// RawResults contains results before a policy
// is applied.
Expand Down Expand Up @@ -253,6 +257,7 @@ type Release struct {

// ReleaseAsset represents a release asset.
type ReleaseAsset struct {
Name string
URL string
Name string
URL string
RekorEntries []models.LogEntryAnon
}
38 changes: 26 additions & 12 deletions checks/evaluation/signed_releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func SignedReleases(name string, dl checker.DetailLogger, r *checker.SignedRelea

totalReleases := 0
totalSigned := 0
totalSigstoreSigned := 0

for _, release := range r.Releases {
if len(release.Assets) == 0 {
Expand All @@ -47,21 +48,34 @@ func SignedReleases(name string, dl checker.DetailLogger, r *checker.SignedRelea

totalReleases++
signed := false
sigstoreSigned := false

for _, asset := range release.Assets {
for _, suffix := range artifactExtensions {
if strings.HasSuffix(asset.Name, suffix) {
dl.Info(&checker.LogMessage{
Path: asset.URL,
Type: checker.FileTypeURL,
Text: fmt.Sprintf("signed release artifact: %s", asset.Name),
})
signed = true
break
// Check if signed with extension
if !signed {
for _, suffix := range artifactExtensions {
if strings.HasSuffix(asset.Name, suffix) {
dl.Info(&checker.LogMessage{
Path: asset.URL,
Type: checker.FileTypeURL,
Text: fmt.Sprintf("signed release artifact: %s", asset.Name),
})
signed = true
totalSigned++
break
}
}
}
if signed {
totalSigned++
// Check if signed with rekor
if !sigstoreSigned {
if len(asset.RekorEntries) != 0 {
// TODO: Do we want to check for a keyless signature? Check if the entry
// contains a Fulcio-issued code-signing certificate.
sigstoreSigned = true
totalSigstoreSigned++
}
}
if sigstoreSigned && signed {
break
}
}
Expand All @@ -86,6 +100,6 @@ func SignedReleases(name string, dl checker.DetailLogger, r *checker.SignedRelea
return checker.CreateInconclusiveResult(name, "no releases found")
}

reason := fmt.Sprintf("%d out of %d artifacts are signed", totalSigned, totalReleases)
reason := fmt.Sprintf("%d out of %d artifacts are signed, %d artifacts are sigstore signed", totalSigned, totalReleases, totalSigstoreSigned)
return checker.CreateProportionalScoreResult(name, reason, totalSigned, totalReleases)
}
92 changes: 90 additions & 2 deletions checks/raw/signed_releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,94 @@
package raw

import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"strings"

"github.com/ossf/scorecard/v4/checker"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/pkg/cosign"

"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/client/index"
"github.com/sigstore/rekor/pkg/generated/models"
)

// Default address of transparency log to search for signing events.
var defaultRekorAddr = "https://rekor.sigstore.dev"

var (
errorRekorSearch = errors.New("error searching rekor entries")
)

func findTLogEntriesByPayload(ctx context.Context, rekorClient *client.Rekor, artifactSha string) (uuids []string, err error) {
params := index.NewSearchIndexParamsWithContext(ctx)
params.Query = &models.SearchIndex{}

params.Query.Hash = fmt.Sprintf("sha256:%s", artifactSha)

searchIndex, err := rekorClient.Index.SearchIndex(params)
if err != nil {
return nil, err
}
return searchIndex.GetPayload(), nil
}

func getRekorEntries(ctx context.Context, rClient *client.Rekor, url string) ([]models.LogEntryAnon, error) {
// URL to hash reader
hasher := sha256.New()
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("error fetching '%v': %w", url, err)
}
defer resp.Body.Close()

if _, err := io.Copy(io.Writer(hasher), resp.Body); err != nil {
return nil, err
}

artifactSha := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))

// Use search index to find rekor entry UUIDs that match Subject Digest.
uuids, err := findTLogEntriesByPayload(ctx, rClient, artifactSha)
if err != nil {
return nil, err
}

// Get and verify log entries
res := []models.LogEntryAnon{}

for _, uuid := range uuids {
e, err := cosign.GetTlogEntry(ctx, rClient, uuid)
if err != nil {
continue
}
if err := cosign.VerifyTLogEntry(ctx, rClient, e); err != nil {
continue
}
res = append(res, *e)
}
return res, nil
}

// SignedReleases checks for presence of signed release check.
func SignedReleases(c *checker.CheckRequest) (checker.SignedReleasesData, error) {
releases, err := c.RepoClient.ListReleases()
if err != nil {
return checker.SignedReleasesData{}, fmt.Errorf("%w", err)
}

rClient, err := rekor.NewClient(defaultRekorAddr)
if err != nil {
// Maybe don't fail on error. Maybe some people use scorecard in weird environments.
return checker.SignedReleasesData{}, fmt.Errorf("%w", err)
}

var results checker.SignedReleasesData
for i, r := range releases {
results.Releases = append(results.Releases,
Expand All @@ -36,9 +112,21 @@ func SignedReleases(c *checker.CheckRequest) (checker.SignedReleasesData, error)
})

for _, asset := range r.Assets {
// Perform a Rekor search index query.
// Only check the latest release, for sake of time.
var entries []models.LogEntryAnon
if rClient != nil && i == 0 {
entries, err = getRekorEntries(c.Ctx, rClient, asset.BrowserDownloadURL)
if err != nil {
// TODO: Just skip if there's an error fetching Rekor entries?
return checker.SignedReleasesData{}, fmt.Errorf("%w", err)
}
}

a := checker.ReleaseAsset{
URL: asset.URL,
Name: asset.Name,
URL: asset.URL,
Name: asset.Name,
RekorEntries: entries,
}
results.Releases[i].Assets = append(results.Releases[i].Assets, a)
}
Expand Down
5 changes: 3 additions & 2 deletions clients/githubrepo/releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ func releasesFrom(data []*github.RepositoryRelease) []clients.Release {
}
for _, a := range r.Assets {
release.Assets = append(release.Assets, clients.ReleaseAsset{
Name: a.GetName(),
URL: a.GetURL(),
Name: a.GetName(),
URL: a.GetURL(),
BrowserDownloadURL: a.GetBrowserDownloadURL(),
})
}
releases = append(releases, release)
Expand Down
5 changes: 3 additions & 2 deletions clients/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Release struct {

// ReleaseAsset is part of the Release bundle.
type ReleaseAsset struct {
Name string
URL string
Name string
URL string
BrowserDownloadURL string
}
Loading