diff --git a/pkg/cmd/attestation/github/attestation.go b/pkg/cmd/attestation/api/attestation.go similarity index 97% rename from pkg/cmd/attestation/github/attestation.go rename to pkg/cmd/attestation/api/attestation.go index 4f48dd4ad..2b96a51fc 100644 --- a/pkg/cmd/attestation/github/attestation.go +++ b/pkg/cmd/attestation/api/attestation.go @@ -1,4 +1,4 @@ -package github +package api import ( "fmt" diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go new file mode 100644 index 000000000..26d5d83c1 --- /dev/null +++ b/pkg/cmd/attestation/api/client.go @@ -0,0 +1,98 @@ +package api + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/cli/cli/v2/api" +) + +const ( + DefaultLimit = 30 + maxLimitForFlag = 1000 + maxLimitForFetch = 100 +) + +type apiClient interface { + REST(hostname, method, p string, body io.Reader, data interface{}) error + RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) +} + +type Client interface { + GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) + GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) +} + +type LiveClient struct { + host string + api apiClient +} + +func NewLiveClient() *LiveClient { + liveAPIClient := api.NewClientFromHTTP(http.DefaultClient) + return &LiveClient{ + host: "https://api.github.com", + api: liveAPIClient, + } +} + +func (c *LiveClient) BuildRepoAndDigestURL(repo, digest string) string { + repo = strings.Trim(repo, "/") + return fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest) +} + +// GetByRepoAndDigest fetches the attestation by repo and digest +func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { + url := c.BuildRepoAndDigestURL(repo, digest) + return c.getAttestations(url, repo, digest, limit) +} + +func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string { + owner = strings.Trim(owner, "/") + return fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest) +} + +// GetByOwnerAndDigest fetches attestation by owner and digest +func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) { + url := c.BuildOwnerAndDigestURL(owner, digest) + return c.getAttestations(url, owner, digest, limit) +} + +func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) { + perPage := limit + if perPage <= 0 || perPage > maxLimitForFlag { + return nil, fmt.Errorf("limit must be greater than 0 and less than or equal to %d", maxLimitForFlag) + } + + if perPage > maxLimitForFetch { + perPage = maxLimitForFetch + } + + // ref: https://github.com/cli/go-gh/blob/d32c104a9a25c9de3d7c7b07a43ae0091441c858/example_gh_test.go#L96 + url = fmt.Sprintf("%s?per_page=%d", url, perPage) + + var attestations []*Attestation + var resp AttestationsResponse + var err error + // if no attestation or less than limit, then keep fetching + for url != "" && len(attestations) < limit { + url, err = c.api.RESTWithNext(c.host, http.MethodGet, url, nil, resp) + if err != nil { + return nil, err + } + + attestations = append(attestations, resp.Attestations...) + } + + if len(resp.Attestations) == 0 { + return nil, newErrNoAttestations(name, digest) + } + + if len(attestations) > limit { + return attestations[:limit], nil + } + + return attestations, nil +} diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go new file mode 100644 index 000000000..662f72f99 --- /dev/null +++ b/pkg/cmd/attestation/api/client_test.go @@ -0,0 +1,176 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testRepo = "github/example" + testOwner = "github" + testDigest = "sha256:12313213" +) + +func NewClientWithMockGHClient() Client { + fetcher := mockDataGenerator{ + NumAttestations: 5, + } + + return &LiveClient{ + api: mockAPIClient{ + OnREST: fetcher.OnRESTSuccess, + OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, + }, + } +} + +func NewClientWithMockGHClientWithNextPage() Client { + fetcher := mockDataGenerator{ + NumAttestations: 5, + } + + return &LiveClient{ + api: mockAPIClient{ + OnREST: fetcher.OnRESTSuccess, + OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, + }, + } +} + +func TestGetURL(t *testing.T) { + c := LiveClient{} + + testData := []struct { + repo string + digest string + expected string + }{ + {repo: "/github/example/", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, + {repo: "/github/example", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, + } + + for _, data := range testData { + s := c.BuildRepoAndDigestURL(data.repo, data.digest) + assert.Equal(t, data.expected, s) + } +} + +func TestGetByDigest(t *testing.T) { + c := NewClientWithMockGHClient() + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) + require.NoError(t, err) + + assert.Equal(t, len(attestations), 5) + bundle := (attestations)[0].Bundle + assert.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit) + require.NoError(t, err) + + assert.Equal(t, len(attestations), 5) + bundle = (attestations)[0].Bundle + assert.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") +} + +func TestGetByDigestGreaterThanLimit(t *testing.T) { + c := NewClientWithMockGHClient() + + limit := 3 + // The method should return five results when the limit is not set + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, limit) + require.NoError(t, err) + + assert.Equal(t, len(attestations), 3) + bundle := (attestations)[0].Bundle + assert.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, limit) + require.NoError(t, err) + + assert.Equal(t, len(attestations), limit) + bundle = (attestations)[0].Bundle + assert.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") +} + +func TestGetByDigestWithNextPage(t *testing.T) { + c := NewClientWithMockGHClientWithNextPage() + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) + require.NoError(t, err) + + assert.Equal(t, len(attestations), 10) + bundle := (attestations)[0].Bundle + assert.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit) + require.NoError(t, err) + + assert.Equal(t, len(attestations), 10) + bundle = (attestations)[0].Bundle + assert.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") +} + +func TestGetByDigestGreaterThanLimitWithNextPage(t *testing.T) { + c := NewClientWithMockGHClientWithNextPage() + + limit := 7 + // The method should return five results when the limit is not set + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, limit) + require.NoError(t, err) + + assert.Equal(t, len(attestations), limit) + bundle := (attestations)[0].Bundle + assert.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, limit) + require.NoError(t, err) + + assert.Equal(t, len(attestations), limit) + bundle = (attestations)[0].Bundle + assert.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") +} + +func TestGetByDigest_NoAttestationsFound(t *testing.T) { + fetcher := mockDataGenerator{ + NumAttestations: 5, + } + + c := LiveClient{ + api: mockAPIClient{ + OnREST: fetcher.OnRESTNoAttestations, + OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, + }, + } + + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) + assert.Error(t, err) + assert.IsType(t, ErrNoAttestations{}, err) + assert.Nil(t, attestations) + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit) + assert.Error(t, err) + assert.IsType(t, ErrNoAttestations{}, err) + assert.Nil(t, attestations) +} + +func TestGetByDigest_Error(t *testing.T) { + fetcher := mockDataGenerator{ + NumAttestations: 5, + } + + c := LiveClient{ + api: mockAPIClient{ + OnREST: fetcher.OnRESTError, + OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, + }, + } + + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) + assert.Error(t, err) + assert.Nil(t, attestations) + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit) + assert.Error(t, err) + assert.Nil(t, attestations) +} diff --git a/pkg/cmd/attestation/api/mock-client.go b/pkg/cmd/attestation/api/mock-client.go new file mode 100644 index 000000000..96a64e4fc --- /dev/null +++ b/pkg/cmd/attestation/api/mock-client.go @@ -0,0 +1,71 @@ +package api + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/sigstore/sigstore-go/pkg/bundle" +) + +type MockClient struct { + OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error) + OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error) +} + +func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { + return m.OnGetByRepoAndDigest(repo, digest, limit) +} + +func (m MockClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) { + return m.OnGetByOwnerAndDigest(owner, digest, limit) +} + +func makeTestAttestation() Attestation { + bundleBytes, err := os.ReadFile("../test/data/sigstore-js-2.1.0-bundle.json") + if err != nil { + panic(err) + } + + var b *bundle.ProtobufBundle + err = json.Unmarshal(bundleBytes, &b) + if err != nil { + panic(err) + } + + return Attestation{Bundle: b} +} + +func OnGetByRepoAndDigestSuccess(repo, digest string, limit int) ([]*Attestation, error) { + att1 := makeTestAttestation() + att2 := makeTestAttestation() + return []*Attestation{&att1, &att2}, nil +} + +func OnGetByRepoAndDigestFailure(repo, digest string, limit int) ([]*Attestation, error) { + return nil, fmt.Errorf("failed to fetch by repo and digest") +} + +func OnGetByOwnerAndDigestSuccess(owner, digest string, limit int) ([]*Attestation, error) { + att1 := makeTestAttestation() + att2 := makeTestAttestation() + return []*Attestation{&att1, &att2}, nil +} + +func OnGetByOwnerAndDigestFailure(owner, digest string, limit int) ([]*Attestation, error) { + return nil, fmt.Errorf("failed to fetch by owner and digest") +} + +func NewTestClient() *MockClient { + return &MockClient{ + OnGetByRepoAndDigest: OnGetByRepoAndDigestSuccess, + OnGetByOwnerAndDigest: OnGetByOwnerAndDigestSuccess, + } +} + +func NewFailTestClient() *MockClient { + return &MockClient{ + OnGetByRepoAndDigest: OnGetByRepoAndDigestFailure, + OnGetByOwnerAndDigest: OnGetByOwnerAndDigestFailure, + } +} diff --git a/pkg/cmd/attestation/api/mock_apiClient_test.go b/pkg/cmd/attestation/api/mock_apiClient_test.go new file mode 100644 index 000000000..08f557f05 --- /dev/null +++ b/pkg/cmd/attestation/api/mock_apiClient_test.go @@ -0,0 +1,152 @@ +package api + +import ( + "errors" + "fmt" + "io" + "strings" +) + +type mockAPIClient struct { + OnREST func(hostname, method, p string, body io.Reader, data interface{}) error + OnRESTWithNext func(hostname, method, p string, body io.Reader, data interface{}) (string, error) +} + +func (m mockAPIClient) REST(hostname, method, p string, body io.Reader, data interface{}) error { + return m.OnREST(hostname, method, p, body, data) +} + +func (m mockAPIClient) RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) { + return m.OnRESTWithNext(hostname, method, p, body, data) +} + +type mockDataGenerator struct { + NumAttestations int +} + +func (m mockDataGenerator) OnRESTSuccess(hostname, method, p string, body io.Reader, data interface{}) error { + return m.OnRESTSuccessHelper(hostname, method, p, body, data, false) +} + +func (m mockDataGenerator) OnRESTSuccessWithNextPage(hostname, method, p string, body io.Reader, data interface{}) (string, error) { + // if path doesn't contain after, it means first time hitting the mock server + // so return the first page and return the link header in the response + if !strings.Contains(p, "after") { + return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, true) + } + + // if path contain after, it means second time hitting the mock server and will not return the link header + return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, false) +} + +func (m mockDataGenerator) OnRESTSuccessHelper(hostname, method, p string, body io.Reader, data interface{}, hasNext bool) error { + atts := make([]*Attestation, m.NumAttestations) + for j := 0; j < m.NumAttestations; j++ { + att := makeTestAttestation() + atts[j] = &att + } + + var resp AttestationsResponse + resp.Attestations = atts + + data = resp + + // // Convert the attestations to JSON + // jsonResponse, err := json.Marshal(resp) + // if err != nil { + // return err + // } + + // // Create a buffer containing the JSON response + // responseReader := bytes.NewBuffer(jsonResponse) + + // linkHeader := "" + // if hasNext { + // // Create a link header with the next page + // linkHeader = fmt.Sprintf("<%s&after=2>; rel=\"next\"", p) + // } + + return nil +} + +func (m mockDataGenerator) OnRESTWithNextSuccessHelper(hostname, method, p string, body io.Reader, data interface{}, hasNext bool) (string, error) { + atts := make([]*Attestation, m.NumAttestations) + for j := 0; j < m.NumAttestations; j++ { + att := makeTestAttestation() + atts[j] = &att + } + + var resp AttestationsResponse + resp.Attestations = atts + + data = resp + + // // Convert the attestations to JSON + // jsonResponse, err := json.Marshal(resp) + // if err != nil { + // return err + // } + + // // Create a buffer containing the JSON response + // responseReader := bytes.NewBuffer(jsonResponse) + + // b, err := io.ReadAll(resp.Body) + // if err != nil { + // return "", err + // } + + // err = json.Unmarshal(b, &data) + // if err != nil { + // return "", err + // } + + + linkHeader := "" + if hasNext { + // Create a link header with the next page + linkHeader = fmt.Sprintf("<%s&after=2>; rel=\"next\"", p) + } + + return linkHeader, nil +} + +func (m mockDataGenerator) OnRESTNoAttestations(hostname, method, p string, body io.Reader, data interface{}) error { + var resp AttestationsResponse + resp.Attestations = make([]*Attestation, 0) + + data = resp + + // Convert the attestations to JSON + // jsonResponse, err := json.Marshal(resp) + // if err != nil { + // return err + // } + + // // Create a buffer containing the JSON response + // responseReader := bytes.NewBuffer(jsonResponse) + + return nil +} + +func (m mockDataGenerator) OnRESTWithNextNoAttestations(hostname, method, p string, body io.Reader, data interface{}) (string, error) { + var resp AttestationsResponse + resp.Attestations = make([]*Attestation, 0) + + data = resp + + // Convert the attestations to JSON + // data, err := json.Marshal(resp) + // if err != nil { + // return err + // } + + return "", nil +} + +func (m mockDataGenerator) OnRESTError(hostname, method, p string, body io.Reader, data interface{}) error { + return errors.New("failed to get attestations") +} + +func (m mockDataGenerator) OnRESTWithNextError(hostname, method, p string, body io.Reader, data interface{}) (string, error) { + return "", errors.New("failed to get attestations") +} diff --git a/pkg/cmd/attestation/artifact/artifact.go b/pkg/cmd/attestation/artifact/artifact.go index 90906da30..dd39eacd5 100644 --- a/pkg/cmd/attestation/artifact/artifact.go +++ b/pkg/cmd/attestation/artifact/artifact.go @@ -63,11 +63,16 @@ func NewDigestedArtifact(client oci.Client, reference, digestAlg string) (artifa return digestLocalFileArtifact(normalized, digestAlg) } -// Digest returns the digest of the artifact +// Digest returns the artifact's digest func (a *DigestedArtifact) Digest() string { return a.digest } +// Algorithm returns the artifact's algorithm +func (a *DigestedArtifact) Algorithm() string { + return a.digestAlg +} + // DigestWithAlg returns the digest:algorithm of the artifact func (a *DigestedArtifact) DigestWithAlg() string { return fmt.Sprintf("%s:%s", a.digestAlg, a.digest) diff --git a/pkg/cmd/attestation/attestation.go b/pkg/cmd/attestation/attestation.go index 7441d6105..a97026956 100644 --- a/pkg/cmd/attestation/attestation.go +++ b/pkg/cmd/attestation/attestation.go @@ -2,8 +2,9 @@ package attestation import ( "github.com/cli/cli/v2/pkg/cmd/attestation/verify" - "github.com/cli/cli/v2/pkg/iostreams" - + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" ) diff --git a/pkg/cmd/attestation/github/client.go b/pkg/cmd/attestation/github/client.go deleted file mode 100644 index 13688a5f4..000000000 --- a/pkg/cmd/attestation/github/client.go +++ /dev/null @@ -1,19 +0,0 @@ -package github - -import "github.com/cli/cli/v2/api" - -type Client interface { - GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) - GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) -} - -type LiveClient struct { - apiClient api.Client -} - -func NewLiveClient() (*LiveClient, error) { - apiClient := api.NewClientFromHTTP(httpClient) - return &LiveClient{ - apiClient: apiClient, - }, nil -} diff --git a/pkg/cmd/attestation/logger/logger.go b/pkg/cmd/attestation/logger/logger.go index 0e5beb62e..8fcd2511f 100644 --- a/pkg/cmd/attestation/logger/logger.go +++ b/pkg/cmd/attestation/logger/logger.go @@ -2,21 +2,89 @@ package logger import ( "fmt" + "log" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/go-gh/v2/pkg/tableprinter" ) type Logger struct { + ColorScheme *iostreams.ColorScheme IO *iostreams.IOStreams - Quiet bool - Verbose bool + quiet bool + verbose bool +} + +func NewLogger(isQuiet, isVerbose bool) *Logger { + io := iostreams.System() + colorScheme := io.ColorScheme() + return &Logger{ + ColorScheme: colorScheme, + IO: io, + quiet: isQuiet, + verbose: isVerbose, + } +} + +// NewDefaultLogger returns a Logger that with the default logging settings +func NewDefaultLogger() *Logger { + isQuiet := false + isVerbose := false + + return NewLogger(isQuiet, isVerbose) +} + +// Printf writes the formatted arguments to the stdout writer. +func (l *Logger) Printf(f string, v ...interface{}) (int, error) { + if l.quiet || !l.IO.IsStdoutTTY() { + return 0, nil + } + return fmt.Fprintf(l.IO.ErrOut, f, v...) +} + +// Println writes the arguments to the stdout writer with a newline at the end. +func (l *Logger) Println(v ...interface{}) (int, error) { + if l.quiet || !l.IO.IsStdoutTTY() { + return 0, nil + } + return fmt.Fprintln(l.IO.ErrOut, v...) } func (l *Logger) VerbosePrint(msg string) (int, error) { - if !l.verbose || !opts.IO.IsStdoutTTY() { + if !l.verbose || !l.IO.IsStdoutTTY() { return 0, nil } return fmt.Fprintf(l.IO.ErrOut, msg) } +func (l *Logger) VerbosePrintf(f string, v ...interface{}) (int, error) { + if !l.verbose || !l.IO.IsStdoutTTY() { + return 0, nil + } + + return fmt.Fprintf(l.IO.ErrOut, f, v...) +} + +func (l *Logger) PrintTableToStdOut(headers []string, rows [][]string) { + if rows == nil { + return + } + t := tableprinter.New(l.IO.Out, l.IO.IsStdoutTTY(), l.IO.TerminalWidth()) + + if headers != nil { + // Print the header row in green + t.AddHeader(headers, tableprinter.WithColor(l.ColorScheme.Green)) + } + + for _, row := range rows { + for _, field := range row { + t.AddField(field, tableprinter.WithTruncate(nil)) + } + t.EndRow() + } + + if err := t.Render(); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json new file mode 100644 index 000000000..d91176e09 --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json @@ -0,0 +1,61 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "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" + } + ] + }, + "tlogEntries": [ + { + "logIndex": "33351527", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "intoto", + "version": "0.0.2" + }, + "integratedTime": "1693323623", + "inclusionPromise": { + "signedEntryTimestamp": "MEYCIQDhWvNSLvnq5ZS3qTIgC7K2uQFeA0g8FEEjNo1UQxeubAIhALDrD1uIiUkk3tQNp/4gKT/9j8zEyyxi9Ti+qaD8q2vI" + }, + "inclusionProof": { + "logIndex": "29188096", + "rootHash": "fbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=", + "treeSize": "29188099", + "hashes": [ + "z7VKeAC2d2x18Vtxt7n40GS3gtc1xfRwjjxxzpy/Trw=", + "/Kd8ZuCLZ7MukSwpjaSgxKFl5X2vdHWk6/qpNrkiUJo=", + "vvkKs7IShUI15nVb9c5olPgBnL9r4uP7+d5KzV0hSjs=", + "Fg4p0WJZw8Lghrpxkx1SXgzfroTeIIrEgEbfgr9IdXY=", + "bH8NahqE+hQ58Qxhg0V5fYusFQ1xsaQ/rK6slWVRT5k=", + "HplNgZ3/afEHq52zcfL2s1AKisOYDdMCWK1jOu1tGyw=", + "uOPuC2YDmwZe989ZN3Lgh5CKMXh9HETrSNgf0jV0WkM=", + "eVsvWKnEZ1+Xo3Ba15DfEiIhhmlrEaIeb+VfYmx8KhY=", + "uLuBRins5nkqq2rqd17R27pQTUF+xetttC6MsmlUzd0=", + "jRUq4D8O+FI47Wbw96s7yHCu4qzWUxpIVfxQEeprDmc=", + "rXEsmEJN4PEoTU8US4qVtdIsGB1MCiRlGOepoiC99kM=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 2605736670972794746\n29188099\nfbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=\nTimestamp: 1693323623528968756\n\n— rekor.sigstore.dev wNI9ajBEAiAK0YTbxRlhOZeeP+844Y+W7iz+hsFIF8x2NYsmuaxifAIgYUFSurlKwN7j5jCwpqSVrkbouQoYIYlyWU9Om16svmI=\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVWQwUkVORFFtcDFaMEYzU1VKQlowbFZRMHBNYVhCVGREQTVTMHhHWXpCS1dXWjFSSEpUWVc0dkwweHpkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BOZDA5RVNUVk5WRlV3VFVSSmVsZG9ZMDVOYWsxM1QwUkpOVTFVVlRGTlJFbDZWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWUVptMDJURkJZVVdWS1ZFTTRPVlZQY1dsT1YyMXVXbGxIYlZnMFZETnBURnBIYVRBS1JWWTBZbVpQYjAwNE5raDZZVGswV0hGNWRYZDRRVzlYY0VOUVpXTkdRMFZpUVdVNGJESmtaeTlsY2pOUE9VeEZSbkZQUTBKV2IzZG5aMVpYVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWbGNYQkRDbGhJY2pOd1kxVmhURE5GUmt0U0swdHpiWFZMVVhGdmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQxbDNXVVJXVWpCU1FWRklMMEpHYTNkV05GcFdZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRETk9jRm96VGpCaU0wcHNURE5PY0FwYU0wNHdZak5LYkV4WGNIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFpoUjFab0NscElUWFppVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQUtZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVSkpSME5wYzBkQlVWRkNaemM0ZDBGUlNVVkNTRUl4WXpKbmQwNW5XVXRMZDFsQ1FrRkhSQXAyZWtGQ1FYZFJiMDFxV210TlZGa3hUVlJOZWs5RVdtMWFiVVpvVG5wcmQxbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZXQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVVZDUVdSVFdsZDRiRmxZVG14TlEwbEhRMmx6UjBGUlVVSm5OemgzUVZGVlJVWklUbkJhTTA0d1lqTktiRXd6VG5BS1dqTk9NR0l6U214TVYzQjZUVUl3UjBOcGMwZEJVVkZDWnpjNGQwRlJXVVZFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUZENaMjl5UW1kRlJRcEJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveFl6SldlVmt5T1hWa1IxWjFDbVJETldwaU1qQjNXbEZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbGhFUmxadlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVbllLWTIxVmRtTXliRzVqTTFKMlkyMVZkR0Z1VFhaTWJXUndaRWRvTVZscE9UTmlNMHB5V20xNGRtUXpUWFpqYlZaeldsZEdlbHBUTlRWaVYzaEJZMjFXYlFwamVUbHZXbGRHYTJONU9YUlpWMngxVFVSblIwTnBjMGRCVVZGQ1p6YzRkMEZSYjBWTFozZHZUV3BhYTAxVVdURk5WRTE2VDBSYWJWcHRSbWhPZW10M0NsbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZrUW1kdmNrSm5SVVZCV1U4dlRVRkZURUpCT0UxRVYyUndaRWRvTVZscE1XOEtZak5PTUZwWFVYZE9kMWxMUzNkWlFrSkJSMFIyZWtGQ1JFRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkZ3BqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZDA5QldVdExkMWxDUWtGSFJIWjZRVUpFVVZGeFJFTm5lVTV0VVhoT2FsVjRUWHBOTkU1dFdtMVpWMFV6Q2s5VVFtbE5WMDE2VFcxWk5VMXFZekZPUkZKdFRWUk5lVTF0VlRCTlZHc3dUVUk0UjBOcGMwZEJVVkZDWnpjNGQwRlJORVZGVVhkUVkyMVdiV041T1c4S1dsZEdhMk41T1hSWlYyeDFUVUpyUjBOcGMwZEJVVkZDWnpjNGQwRlJPRVZEZDNkS1RrUnJNVTVVWXpCT1ZGVXhUVU56UjBOcGMwZEJVVkZDWnpjNGR3cEJVa0ZGU0ZGM1ltRklVakJqU0UwMlRIazVibUZZVW05a1YwbDFXVEk1ZEV3elRuQmFNMDR3WWpOS2JFMUNaMGREYVhOSFFWRlJRbWMzT0hkQlVrVkZDa05uZDBsT2VrVjNUMVJaZWs1VVRYZGFVVmxMUzNkWlFrSkJSMFIyZWtGQ1JXZFNXRVJHVm05a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFlLWXpKc2JtTXpVblpqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZGt4dFpIQmtSMmd4V1drNU0ySXpTbkphYlhoMlpETk5kbU50Vm5OYVYwWjZXbE0xTlFwaVYzaEJZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOU5hbHByVFZSWk1VMVVUWHBQUkZwdENscHRSbWhPZW10M1dXcEdhazE2U20xUFZFa3pUbFJSTUZwcVJYcE5ha3BzVGtSRk5VNUVRVlZDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0ZaVFVKSVFqRUtZekpuZDFkbldVdExkMWxDUWtGSFJIWjZRVUpHVVZKTlJFVndiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtTXliRzVqTTFKMlkyMVZkZ3BqTW14dVl6TlNkbU50VlhSaGJrMTJXVmRPTUdGWE9YVmplVGw1WkZjMWVreDZXWGROVkZFd1QwUm5NazVxV1haWldGSXdXbGN4ZDJSSVRYWk5WRUZYQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVmRDUVdkTlFtNUNNVmx0ZUhCWmVrTkNhV2RaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamhDU0c5QlpVRkNNa0ZPTURrS1RVZHlSM2g0UlhsWmVHdGxTRXBzYms1M1MybFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKcGEwaDZLM1ozUVVGQlVVUkJSV04zVWxGSlp3cGFSVzg0WXpCbFExcElSV2cwZFhwNlNucEdlamxVSzBWbVUxUk9WSFJDTWtaSlNERTRkbGh3YTA5elEwbFJSRVV4VFZSMGFUbFNiMFJ1VWs4elUwVlVDakZhYTJGa05rWnZWSGd2YXpaNmRGRmpkMGxFVUcxdVVuaFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXVRVVJDYTBGcVFrSXdObVp0VGxoNE5sUnZZVU1LYkVabk1tdFBlRzVtVEVkbmNuWnZVak5HTlVkcVJIUjJSRUpDT0cwNVUxZFJUbnBNTWpFeGFsbHRVeTluSzFsaVlubFZRMDFESzJGa05tcEpTeXRsWmdwbE5GaFBTV3hvVEdOWGVHVmFZa0owVFdwTFUzSlFlRzF0TkdwU00wSkdVVkZQUWxJck9ISXlOME5wYjNsb2VHOVRjWFpNZFhjOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdCIsInNpZyI6IlRVVlJRMGxEWVhSb2JsUkljMlJtV25GSWNUTnBXRXhxVFdVd1ZGVTViRXhaYmxJeGVtazNNM0JSZFZobU5VdElRV2xDTWpOS2FDdG5VMll4VVVWRGJrRlRSMlZ3TW1VeVRVZHVVRmhwYjFWTVZqUjJRMGxUU2tKdWEwcGFaejA5In1dfSwiaGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjFiZDg4ZTA1NGY2OGE3ZDE3MzkzNjcyYjAzZmExOGZkNjRmMGRjZDVmY2M1NDAzNWMxOWZlNjQwMWNmMmNmNWUifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4Y2ViNGFiODEyNzczMTQ3M2E5ZWM4MTE0MGNiNjg0OWNmOGU5NzBjZGEzMmJhZWYwOTlkZjQ4YmEzMjY0NDQyIn19fX0=" + } + ], + "timestampVerificationData": null + }, + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOm5wbS9zaWdzdG9yZUAyLjEuMCIsImRpZ2VzdCI6eyJzaGE1MTIiOiI5MGYyMjNmOTkyZTRjODhkZDA2OGNkMmE1ZmM1N2Y5ZDJiMzA3OTgzNDNkZDZlMzhmMjljMjQwZTA0YmEwOTBlZjgzMWY4NDQ5MDg0N2M0ZTgyYjkyMzJjNzhlOGEyNTg0NjNiMWU1NWMwZjc0NjlmNzMwMjY1MDA4ZmE2NjMzZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vc2xzYS1mcmFtZXdvcmsuZ2l0aHViLmlvL2dpdGh1Yi1hY3Rpb25zLWJ1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcyIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiI0OTU1NzQ1NTUiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiNzEwOTYzNTMifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzQHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiIyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0In19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcy9hY3Rpb25zL3J1bnMvNjAxNDQ4ODY2Ni9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEQCICathnTHsdfZqHq3iXLjMe0TU9lLYnR1zi73pQuXf5KHAiB23Jh+gSf1QECnASGep2e2MGnPXioULV4vCISJBnkJZg==", + "keyid": "" + } + ] + } + } diff --git a/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz new file mode 100644 index 000000000..390b823fd Binary files /dev/null and b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz differ diff --git a/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl new file mode 100644 index 000000000..a4aca34d0 --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl @@ -0,0 +1,2 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle+json;version=0.1","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"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"}]},"tlogEntries":[{"logIndex":"33351527","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"intoto","version":"0.0.2"},"integratedTime":"1693323623","inclusionPromise":{"signedEntryTimestamp":"MEYCIQDhWvNSLvnq5ZS3qTIgC7K2uQFeA0g8FEEjNo1UQxeubAIhALDrD1uIiUkk3tQNp/4gKT/9j8zEyyxi9Ti+qaD8q2vI"},"inclusionProof":{"logIndex":"29188096","rootHash":"fbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=","treeSize":"29188099","hashes":["z7VKeAC2d2x18Vtxt7n40GS3gtc1xfRwjjxxzpy/Trw=","/Kd8ZuCLZ7MukSwpjaSgxKFl5X2vdHWk6/qpNrkiUJo=","vvkKs7IShUI15nVb9c5olPgBnL9r4uP7+d5KzV0hSjs=","Fg4p0WJZw8Lghrpxkx1SXgzfroTeIIrEgEbfgr9IdXY=","bH8NahqE+hQ58Qxhg0V5fYusFQ1xsaQ/rK6slWVRT5k=","HplNgZ3/afEHq52zcfL2s1AKisOYDdMCWK1jOu1tGyw=","uOPuC2YDmwZe989ZN3Lgh5CKMXh9HETrSNgf0jV0WkM=","eVsvWKnEZ1+Xo3Ba15DfEiIhhmlrEaIeb+VfYmx8KhY=","uLuBRins5nkqq2rqd17R27pQTUF+xetttC6MsmlUzd0=","jRUq4D8O+FI47Wbw96s7yHCu4qzWUxpIVfxQEeprDmc=","rXEsmEJN4PEoTU8US4qVtdIsGB1MCiRlGOepoiC99kM="],"checkpoint":{"envelope":"rekor.sigstore.dev - 2605736670972794746\n29188099\nfbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=\nTimestamp: 1693323623528968756\n\n— rekor.sigstore.dev wNI9ajBEAiAK0YTbxRlhOZeeP+844Y+W7iz+hsFIF8x2NYsmuaxifAIgYUFSurlKwN7j5jCwpqSVrkbouQoYIYlyWU9Om16svmI=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVWQwUkVORFFtcDFaMEYzU1VKQlowbFZRMHBNYVhCVGREQTVTMHhHWXpCS1dXWjFSSEpUWVc0dkwweHpkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BOZDA5RVNUVk5WRlV3VFVSSmVsZG9ZMDVOYWsxM1QwUkpOVTFVVlRGTlJFbDZWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWUVptMDJURkJZVVdWS1ZFTTRPVlZQY1dsT1YyMXVXbGxIYlZnMFZETnBURnBIYVRBS1JWWTBZbVpQYjAwNE5raDZZVGswV0hGNWRYZDRRVzlYY0VOUVpXTkdRMFZpUVdVNGJESmtaeTlsY2pOUE9VeEZSbkZQUTBKV2IzZG5aMVpYVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWbGNYQkRDbGhJY2pOd1kxVmhURE5GUmt0U0swdHpiWFZMVVhGdmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQxbDNXVVJXVWpCU1FWRklMMEpHYTNkV05GcFdZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRETk9jRm96VGpCaU0wcHNURE5PY0FwYU0wNHdZak5LYkV4WGNIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFpoUjFab0NscElUWFppVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQUtZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVSkpSME5wYzBkQlVWRkNaemM0ZDBGUlNVVkNTRUl4WXpKbmQwNW5XVXRMZDFsQ1FrRkhSQXAyZWtGQ1FYZFJiMDFxV210TlZGa3hUVlJOZWs5RVdtMWFiVVpvVG5wcmQxbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZXQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVVZDUVdSVFdsZDRiRmxZVG14TlEwbEhRMmx6UjBGUlVVSm5OemgzUVZGVlJVWklUbkJhTTA0d1lqTktiRXd6VG5BS1dqTk9NR0l6U214TVYzQjZUVUl3UjBOcGMwZEJVVkZDWnpjNGQwRlJXVVZFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUZENaMjl5UW1kRlJRcEJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveFl6SldlVmt5T1hWa1IxWjFDbVJETldwaU1qQjNXbEZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbGhFUmxadlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVbllLWTIxVmRtTXliRzVqTTFKMlkyMVZkR0Z1VFhaTWJXUndaRWRvTVZscE9UTmlNMHB5V20xNGRtUXpUWFpqYlZaeldsZEdlbHBUTlRWaVYzaEJZMjFXYlFwamVUbHZXbGRHYTJONU9YUlpWMngxVFVSblIwTnBjMGRCVVZGQ1p6YzRkMEZSYjBWTFozZHZUV3BhYTAxVVdURk5WRTE2VDBSYWJWcHRSbWhPZW10M0NsbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZrUW1kdmNrSm5SVVZCV1U4dlRVRkZURUpCT0UxRVYyUndaRWRvTVZscE1XOEtZak5PTUZwWFVYZE9kMWxMUzNkWlFrSkJSMFIyZWtGQ1JFRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkZ3BqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZDA5QldVdExkMWxDUWtGSFJIWjZRVUpFVVZGeFJFTm5lVTV0VVhoT2FsVjRUWHBOTkU1dFdtMVpWMFV6Q2s5VVFtbE5WMDE2VFcxWk5VMXFZekZPUkZKdFRWUk5lVTF0VlRCTlZHc3dUVUk0UjBOcGMwZEJVVkZDWnpjNGQwRlJORVZGVVhkUVkyMVdiV041T1c4S1dsZEdhMk41T1hSWlYyeDFUVUpyUjBOcGMwZEJVVkZDWnpjNGQwRlJPRVZEZDNkS1RrUnJNVTVVWXpCT1ZGVXhUVU56UjBOcGMwZEJVVkZDWnpjNGR3cEJVa0ZGU0ZGM1ltRklVakJqU0UwMlRIazVibUZZVW05a1YwbDFXVEk1ZEV3elRuQmFNMDR3WWpOS2JFMUNaMGREYVhOSFFWRlJRbWMzT0hkQlVrVkZDa05uZDBsT2VrVjNUMVJaZWs1VVRYZGFVVmxMUzNkWlFrSkJSMFIyZWtGQ1JXZFNXRVJHVm05a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFlLWXpKc2JtTXpVblpqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZGt4dFpIQmtSMmd4V1drNU0ySXpTbkphYlhoMlpETk5kbU50Vm5OYVYwWjZXbE0xTlFwaVYzaEJZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOU5hbHByVFZSWk1VMVVUWHBQUkZwdENscHRSbWhPZW10M1dXcEdhazE2U20xUFZFa3pUbFJSTUZwcVJYcE5ha3BzVGtSRk5VNUVRVlZDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0ZaVFVKSVFqRUtZekpuZDFkbldVdExkMWxDUWtGSFJIWjZRVUpHVVZKTlJFVndiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtTXliRzVqTTFKMlkyMVZkZ3BqTW14dVl6TlNkbU50VlhSaGJrMTJXVmRPTUdGWE9YVmplVGw1WkZjMWVreDZXWGROVkZFd1QwUm5NazVxV1haWldGSXdXbGN4ZDJSSVRYWk5WRUZYQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVmRDUVdkTlFtNUNNVmx0ZUhCWmVrTkNhV2RaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamhDU0c5QlpVRkNNa0ZPTURrS1RVZHlSM2g0UlhsWmVHdGxTRXBzYms1M1MybFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKcGEwaDZLM1ozUVVGQlVVUkJSV04zVWxGSlp3cGFSVzg0WXpCbFExcElSV2cwZFhwNlNucEdlamxVSzBWbVUxUk9WSFJDTWtaSlNERTRkbGh3YTA5elEwbFJSRVV4VFZSMGFUbFNiMFJ1VWs4elUwVlVDakZhYTJGa05rWnZWSGd2YXpaNmRGRmpkMGxFVUcxdVVuaFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXVRVVJDYTBGcVFrSXdObVp0VGxoNE5sUnZZVU1LYkVabk1tdFBlRzVtVEVkbmNuWnZVak5HTlVkcVJIUjJSRUpDT0cwNVUxZFJUbnBNTWpFeGFsbHRVeTluSzFsaVlubFZRMDFESzJGa05tcEpTeXRsWmdwbE5GaFBTV3hvVEdOWGVHVmFZa0owVFdwTFUzSlFlRzF0TkdwU00wSkdVVkZQUWxJck9ISXlOME5wYjNsb2VHOVRjWFpNZFhjOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdCIsInNpZyI6IlRVVlJRMGxEWVhSb2JsUkljMlJtV25GSWNUTnBXRXhxVFdVd1ZGVTViRXhaYmxJeGVtazNNM0JSZFZobU5VdElRV2xDTWpOS2FDdG5VMll4VVVWRGJrRlRSMlZ3TW1VeVRVZHVVRmhwYjFWTVZqUjJRMGxUU2tKdWEwcGFaejA5In1dfSwiaGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjFiZDg4ZTA1NGY2OGE3ZDE3MzkzNjcyYjAzZmExOGZkNjRmMGRjZDVmY2M1NDAzNWMxOWZlNjQwMWNmMmNmNWUifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4Y2ViNGFiODEyNzczMTQ3M2E5ZWM4MTE0MGNiNjg0OWNmOGU5NzBjZGEzMmJhZWYwOTlkZjQ4YmEzMjY0NDQyIn19fX0="}],"timestampVerificationData":null},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOm5wbS9zaWdzdG9yZUAyLjEuMCIsImRpZ2VzdCI6eyJzaGE1MTIiOiI5MGYyMjNmOTkyZTRjODhkZDA2OGNkMmE1ZmM1N2Y5ZDJiMzA3OTgzNDNkZDZlMzhmMjljMjQwZTA0YmEwOTBlZjgzMWY4NDQ5MDg0N2M0ZTgyYjkyMzJjNzhlOGEyNTg0NjNiMWU1NWMwZjc0NjlmNzMwMjY1MDA4ZmE2NjMzZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vc2xzYS1mcmFtZXdvcmsuZ2l0aHViLmlvL2dpdGh1Yi1hY3Rpb25zLWJ1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcyIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiI0OTU1NzQ1NTUiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiNzEwOTYzNTMifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzQHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiIyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0In19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcy9hY3Rpb25zL3J1bnMvNjAxNDQ4ODY2Ni9hdHRlbXB0cy8xIn19fX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEQCICathnTHsdfZqHq3iXLjMe0TU9lLYnR1zi73pQuXf5KHAiB23Jh+gSf1QECnASGep2e2MGnPXioULV4vCISJBnkJZg==","keyid":""}]}} +{"mediaType":"application/vnd.dev.sigstore.bundle+json;version=0.1","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"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"}]},"tlogEntries":[{"logIndex":"33351527","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"intoto","version":"0.0.2"},"integratedTime":"1693323623","inclusionPromise":{"signedEntryTimestamp":"MEYCIQDhWvNSLvnq5ZS3qTIgC7K2uQFeA0g8FEEjNo1UQxeubAIhALDrD1uIiUkk3tQNp/4gKT/9j8zEyyxi9Ti+qaD8q2vI"},"inclusionProof":{"logIndex":"29188096","rootHash":"fbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=","treeSize":"29188099","hashes":["z7VKeAC2d2x18Vtxt7n40GS3gtc1xfRwjjxxzpy/Trw=","/Kd8ZuCLZ7MukSwpjaSgxKFl5X2vdHWk6/qpNrkiUJo=","vvkKs7IShUI15nVb9c5olPgBnL9r4uP7+d5KzV0hSjs=","Fg4p0WJZw8Lghrpxkx1SXgzfroTeIIrEgEbfgr9IdXY=","bH8NahqE+hQ58Qxhg0V5fYusFQ1xsaQ/rK6slWVRT5k=","HplNgZ3/afEHq52zcfL2s1AKisOYDdMCWK1jOu1tGyw=","uOPuC2YDmwZe989ZN3Lgh5CKMXh9HETrSNgf0jV0WkM=","eVsvWKnEZ1+Xo3Ba15DfEiIhhmlrEaIeb+VfYmx8KhY=","uLuBRins5nkqq2rqd17R27pQTUF+xetttC6MsmlUzd0=","jRUq4D8O+FI47Wbw96s7yHCu4qzWUxpIVfxQEeprDmc=","rXEsmEJN4PEoTU8US4qVtdIsGB1MCiRlGOepoiC99kM="],"checkpoint":{"envelope":"rekor.sigstore.dev - 2605736670972794746\n29188099\nfbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=\nTimestamp: 1693323623528968756\n\n— rekor.sigstore.dev wNI9ajBEAiAK0YTbxRlhOZeeP+844Y+W7iz+hsFIF8x2NYsmuaxifAIgYUFSurlKwN7j5jCwpqSVrkbouQoYIYlyWU9Om16svmI=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVWQwUkVORFFtcDFaMEYzU1VKQlowbFZRMHBNYVhCVGREQTVTMHhHWXpCS1dXWjFSSEpUWVc0dkwweHpkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BOZDA5RVNUVk5WRlV3VFVSSmVsZG9ZMDVOYWsxM1QwUkpOVTFVVlRGTlJFbDZWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWUVptMDJURkJZVVdWS1ZFTTRPVlZQY1dsT1YyMXVXbGxIYlZnMFZETnBURnBIYVRBS1JWWTBZbVpQYjAwNE5raDZZVGswV0hGNWRYZDRRVzlYY0VOUVpXTkdRMFZpUVdVNGJESmtaeTlsY2pOUE9VeEZSbkZQUTBKV2IzZG5aMVpYVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWbGNYQkRDbGhJY2pOd1kxVmhURE5GUmt0U0swdHpiWFZMVVhGdmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQxbDNXVVJXVWpCU1FWRklMMEpHYTNkV05GcFdZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRETk9jRm96VGpCaU0wcHNURE5PY0FwYU0wNHdZak5LYkV4WGNIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFpoUjFab0NscElUWFppVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQUtZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVSkpSME5wYzBkQlVWRkNaemM0ZDBGUlNVVkNTRUl4WXpKbmQwNW5XVXRMZDFsQ1FrRkhSQXAyZWtGQ1FYZFJiMDFxV210TlZGa3hUVlJOZWs5RVdtMWFiVVpvVG5wcmQxbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZXQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVVZDUVdSVFdsZDRiRmxZVG14TlEwbEhRMmx6UjBGUlVVSm5OemgzUVZGVlJVWklUbkJhTTA0d1lqTktiRXd6VG5BS1dqTk9NR0l6U214TVYzQjZUVUl3UjBOcGMwZEJVVkZDWnpjNGQwRlJXVVZFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUZENaMjl5UW1kRlJRcEJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveFl6SldlVmt5T1hWa1IxWjFDbVJETldwaU1qQjNXbEZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbGhFUmxadlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVbllLWTIxVmRtTXliRzVqTTFKMlkyMVZkR0Z1VFhaTWJXUndaRWRvTVZscE9UTmlNMHB5V20xNGRtUXpUWFpqYlZaeldsZEdlbHBUTlRWaVYzaEJZMjFXYlFwamVUbHZXbGRHYTJONU9YUlpWMngxVFVSblIwTnBjMGRCVVZGQ1p6YzRkMEZSYjBWTFozZHZUV3BhYTAxVVdURk5WRTE2VDBSYWJWcHRSbWhPZW10M0NsbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZrUW1kdmNrSm5SVVZCV1U4dlRVRkZURUpCT0UxRVYyUndaRWRvTVZscE1XOEtZak5PTUZwWFVYZE9kMWxMUzNkWlFrSkJSMFIyZWtGQ1JFRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkZ3BqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZDA5QldVdExkMWxDUWtGSFJIWjZRVUpFVVZGeFJFTm5lVTV0VVhoT2FsVjRUWHBOTkU1dFdtMVpWMFV6Q2s5VVFtbE5WMDE2VFcxWk5VMXFZekZPUkZKdFRWUk5lVTF0VlRCTlZHc3dUVUk0UjBOcGMwZEJVVkZDWnpjNGQwRlJORVZGVVhkUVkyMVdiV041T1c4S1dsZEdhMk41T1hSWlYyeDFUVUpyUjBOcGMwZEJVVkZDWnpjNGQwRlJPRVZEZDNkS1RrUnJNVTVVWXpCT1ZGVXhUVU56UjBOcGMwZEJVVkZDWnpjNGR3cEJVa0ZGU0ZGM1ltRklVakJqU0UwMlRIazVibUZZVW05a1YwbDFXVEk1ZEV3elRuQmFNMDR3WWpOS2JFMUNaMGREYVhOSFFWRlJRbWMzT0hkQlVrVkZDa05uZDBsT2VrVjNUMVJaZWs1VVRYZGFVVmxMUzNkWlFrSkJSMFIyZWtGQ1JXZFNXRVJHVm05a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFlLWXpKc2JtTXpVblpqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZGt4dFpIQmtSMmd4V1drNU0ySXpTbkphYlhoMlpETk5kbU50Vm5OYVYwWjZXbE0xTlFwaVYzaEJZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOU5hbHByVFZSWk1VMVVUWHBQUkZwdENscHRSbWhPZW10M1dXcEdhazE2U20xUFZFa3pUbFJSTUZwcVJYcE5ha3BzVGtSRk5VNUVRVlZDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0ZaVFVKSVFqRUtZekpuZDFkbldVdExkMWxDUWtGSFJIWjZRVUpHVVZKTlJFVndiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtTXliRzVqTTFKMlkyMVZkZ3BqTW14dVl6TlNkbU50VlhSaGJrMTJXVmRPTUdGWE9YVmplVGw1WkZjMWVreDZXWGROVkZFd1QwUm5NazVxV1haWldGSXdXbGN4ZDJSSVRYWk5WRUZYQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVmRDUVdkTlFtNUNNVmx0ZUhCWmVrTkNhV2RaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamhDU0c5QlpVRkNNa0ZPTURrS1RVZHlSM2g0UlhsWmVHdGxTRXBzYms1M1MybFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKcGEwaDZLM1ozUVVGQlVVUkJSV04zVWxGSlp3cGFSVzg0WXpCbFExcElSV2cwZFhwNlNucEdlamxVSzBWbVUxUk9WSFJDTWtaSlNERTRkbGh3YTA5elEwbFJSRVV4VFZSMGFUbFNiMFJ1VWs4elUwVlVDakZhYTJGa05rWnZWSGd2YXpaNmRGRmpkMGxFVUcxdVVuaFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXVRVVJDYTBGcVFrSXdObVp0VGxoNE5sUnZZVU1LYkVabk1tdFBlRzVtVEVkbmNuWnZVak5HTlVkcVJIUjJSRUpDT0cwNVUxZFJUbnBNTWpFeGFsbHRVeTluSzFsaVlubFZRMDFESzJGa05tcEpTeXRsWmdwbE5GaFBTV3hvVEdOWGVHVmFZa0owVFdwTFUzSlFlRzF0TkdwU00wSkdVVkZQUWxJck9ISXlOME5wYjNsb2VHOVRjWFpNZFhjOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdCIsInNpZyI6IlRVVlJRMGxEWVhSb2JsUkljMlJtV25GSWNUTnBXRXhxVFdVd1ZGVTViRXhaYmxJeGVtazNNM0JSZFZobU5VdElRV2xDTWpOS2FDdG5VMll4VVVWRGJrRlRSMlZ3TW1VeVRVZHVVRmhwYjFWTVZqUjJRMGxUU2tKdWEwcGFaejA5In1dfSwiaGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjFiZDg4ZTA1NGY2OGE3ZDE3MzkzNjcyYjAzZmExOGZkNjRmMGRjZDVmY2M1NDAzNWMxOWZlNjQwMWNmMmNmNWUifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4Y2ViNGFiODEyNzczMTQ3M2E5ZWM4MTE0MGNiNjg0OWNmOGU5NzBjZGEzMmJhZWYwOTlkZjQ4YmEzMjY0NDQyIn19fX0="}],"timestampVerificationData":null},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOm5wbS9zaWdzdG9yZUAyLjEuMCIsImRpZ2VzdCI6eyJzaGE1MTIiOiI5MGYyMjNmOTkyZTRjODhkZDA2OGNkMmE1ZmM1N2Y5ZDJiMzA3OTgzNDNkZDZlMzhmMjljMjQwZTA0YmEwOTBlZjgzMWY4NDQ5MDg0N2M0ZTgyYjkyMzJjNzhlOGEyNTg0NjNiMWU1NWMwZjc0NjlmNzMwMjY1MDA4ZmE2NjMzZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vc2xzYS1mcmFtZXdvcmsuZ2l0aHViLmlvL2dpdGh1Yi1hY3Rpb25zLWJ1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcyIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiI0OTU1NzQ1NTUiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiNzEwOTYzNTMifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzQHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiIyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0In19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcy9hY3Rpb25zL3J1bnMvNjAxNDQ4ODY2Ni9hdHRlbXB0cy8xIn19fX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEQCICathnTHsdfZqHq3iXLjMe0TU9lLYnR1zi73pQuXf5KHAiB23Jh+gSf1QECnASGep2e2MGnPXioULV4vCISJBnkJZg==","keyid":""}]}} diff --git a/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-mismatched-signatures.json b/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-mismatched-signatures.json new file mode 100644 index 000000000..43e9bfac3 --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-mismatched-signatures.json @@ -0,0 +1,48 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "tlogEntries": [ + { + "logIndex": "6755099", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "intoto", + "version": "0.0.2" + }, + "integratedTime": "1667953968", + "inclusionPromise": { + "signedEntryTimestamp": "MEUCIACiV07L0UdN6++1ljz5fbK9L5w3epXGvb/tRRmg+LlOAiEA+ksZQyovogZd2Y2YH0A9iXle3Cee90F/TU2h9DcdkbA=" + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoidGV4dC9wbGFpbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVU52YWtORFFXbGxaMEYzU1VKQlowbFZXbXMyUWpGRE56UjZURXBMTDBOblMwMWhOV3AyVmtaWk9IZE5kME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BKZUUxVVFUVk5SRUY2VFdwUk0xZG9ZMDVOYWtsNFRWUkJOVTFFUVRCTmFsRXpWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWb1psWkVla0ZoZWxjM1RERjVPREE1TVdObFNtWmFUbU16VmxWTll6VlpRVEpNWTNvS1pqUjNhRXBQVjJ4RGNuZExXVE5IUXpKTlVpdEVWbEp6YVZGck1HRTBWRUpWYkhWT2F6Sk5hM1pOZHpodE9VdEllalpQUTBGVldYZG5aMFpEVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWd2FFSjFDa2xLTjBGbWVucGlMMVpTUmt0WWNIRm5aMUUzUW1kQmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQwaDNXVVJXVWpCU1FWRklMMEpDVlhkRk5FVlNXVzVLY0ZsWE5VRmFSMVp2V1ZjeGJHTnBOV3BpTWpCM1RFRlpTMHQzV1VKQ1FVZEVkbnBCUWdwQlVWRmxZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRESjRkbG95YkhWTU1qbG9aRmhTYjAxSlIweENaMjl5UW1kRlJVRmtXalZCWjFGRENrSklNRVZsZDBJMVFVaGpRVE5VTUhkaGMySklSVlJLYWtkU05HTnRWMk16UVhGS1MxaHlhbVZRU3pNdmFEUndlV2RET0hBM2J6UkJRVUZIUlZkak0wUUthMmRCUVVKQlRVRlRSRUpIUVdsRlFTdFliemxHZGl0dVpHTlpNazlzTldJeVRUaEZTSGhUTTBob2IwSmlTalpTUWpKVVkzWXZURTAwZVZGRFNWRkVaQXBzY1c1U2JFbzVWWEJWTkdwT2VXaGhRbE5FWjBSMGVEY3JPRTVSTVVkdU56RTBjMlJZVVdGdlJtcEJTMEpuWjNGb2EycFBVRkZSUkVGM1RuQkJSRUp0Q2tGcVJVRnViWGRXWlZGMVJYTnhaMVV4U0hKS1RrSlZaSEIxTlhVMlozZFhSSFExVGxWR09YcFphWFI1YUZWTlQwZFdTMWRRUmxKek1YcFJlVEkyYldjS1JtZ3hjMEZxUlVGM1ZqRlRabGRtTnk5QlNUWllRVzVZYUdNMFZrSllRaTlKVFhkVlFuSmxjbEJ4WVVwS2NWRTNSblF4ZGtKQ2JpOXFSRkpaYVhOelJ3cDFjVWx5V1VGWFN3b3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0iLCJzaWciOiJUVVZWUTBsQ1ZUSldiMFUxV0c5VE9EQkZWamhxVHpCU2RsRmhWRzFsUWtWTFIwVjBWVUZKYTJKa1FsUnNXV3QxUVdsRlFXNDJkazl6VkRsNlJWcHpSemhpV1ROdlJUVTBXVWhsTHpWQmNFTXZXVFZwVFZCWWRqaGhjREIyUlZFOSJ9XX0sImhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI5ODQ2YjRlMDU0ZWYwYzVlZWIwNzA1NDQ0N2I2OWQ2ODgxZWRkNmIwMTZlOWU4Nzg0M2YyMzVhOWMyMDFkMzAzIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiNjhlNjU2YjI1MWU2N2U4MzU4YmVmODQ4M2FiMGQ1MWM2NjE5ZjNlN2ExYTlmMGU3NTgzOGQ0MWZmMzY4ZjcyOCJ9fX19" + } + ], + "timestampVerificationData": { + "rfc3161Timestamps": [] + }, + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "MIICoDCCAiagAwIBAgIUSkFzCwp0c8PRty4So7bizLRExZswCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjIxMTA5MDAyOTI1WhcNMjIxMTA5MDAzOTI1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEj/l/5Pi+TWUkamLY1UH9yq0XgeH818M38IQm6rYdeT0JLLRozmzqGGUjoAFw1G69fBTAP0ae22V93BFT2JGYjqOCAUUwggFBMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUZzrPkPRD8aCDHvxscd7v6T5fOZMwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0RAQH/BBUwE4ERYnJpYW5AZGVoYW1lci5jb20wLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGEWcqsQgAABAMARzBFAiEA0XEwx48N/TZneFxEV7RwbVIhROPOMJYo1XEoN4sd98wCIGlL4RnI8JVHdYbzbZLHUIr7+5I1Dl//zUJBh4A3YPnzMAoGCCqGSM49BAMDA2gAMGUCMQDyFvX31sp+olif92nr/w52KLmtPoWVHxtGeaxuNAtQ7DFdtSDvXGenUT+fqALnAikCMD/hbwNuQl9kr2nlZ96GlT3eMCl78o0D2QKjx2Do1JC6lqG2hDM61FG6Znx7T5VWfQ==" + }, + { + "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" + } + ] + } + }, + "dsseEnvelope": { + "payload": "aGVsbG8sIHdvcmxkIQ==", + "payloadType": "text/plain", + "signatures": [ + { + "sig": "MEYCIQDBfvwEED9oUrvTjZ7cJrZkE073IqG8V8r5I1wMY3YAJwIhAJ+UXICfI5rs7Jl2oFEAK8KG+asRWQA+ZkRA1jW2lUYO", + "keyid": "" + } + ] + } +} diff --git a/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-signature.json b/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-signature.json new file mode 100644 index 000000000..b1275cebc --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-signature.json @@ -0,0 +1,48 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "tlogEntries": [ + { + "logIndex": "6751924", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "intoto", + "version": "0.0.2" + }, + "integratedTime": "1667948287", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCIEzguFRaGzOpMw9JJGUfqSJQ11qlzpcyVCkZfZYPwpLCAiBzdU4LnjtVKYCfyoTImFh3OLFWeOKygtS47Z8fp1GYHg==" + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoidGV4dC9wbGFpbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVU51ZWtORFFXbFhaMEYzU1VKQlowbFZWa2gzWldoUGRFZHVORXRUUkRGSU9GSkpOVGd4VFdaaWVXVjNkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BKZUUxVVFUUk5ha2t4VDBSQk1sZG9ZMDVOYWtsNFRWUkJORTFxVFhkUFJFRXlWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWSFp6WklhbmgwTWxWT2FVb3hhM2QzY1RWWVVVbEpkMDFhYmtwbVZsRXpZa1l3TVhVS1drdDBaVTFrWTFZdk0zRm9RMjFYVDJWamIzaFNjWGR5WWxsVWMyaEhaemxPZVZoalFtSjJaVFo2UzNkYVZsUk1aWEZQUTBGVlVYZG5aMFpCVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZVM1YzQlNDall3YzBOd1oyWjFNRFIzWTNOcWRrTkdlSFF3WmsxcmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQwaDNXVVJXVWpCU1FWRklMMEpDVlhkRk5FVlNXVzVLY0ZsWE5VRmFSMVp2V1ZjeGJHTnBOV3BpTWpCM1RFRlpTMHQzV1VKQ1FVZEVkbnBCUWdwQlVWRmxZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRESjRkbG95YkhWTU1qbG9aRmhTYjAxSlIwcENaMjl5UW1kRlJVRmtXalZCWjFGRENrSkljMFZsVVVJelFVaFZRVE5VTUhkaGMySklSVlJLYWtkU05HTnRWMk16UVhGS1MxaHlhbVZRU3pNdmFEUndlV2RET0hBM2J6UkJRVUZIUlZkWVkxSUtPRUZCUVVKQlRVRlNha0pGUVdsQ1VsUnlSMFUxV1RGRmJsbHVhV0ZLUWl0dWMzWTRPVlpoV1hnelVWcHFiMk5GYVc0emNqa3hkMlpyUVVsblRYTnpLd3BtYzNOMU5WTk1VV3R1TjFkRVZFdFlaMjkzTjFONFlraFpVMXBxTTNscmVFRnlWbTUxZWtWM1EyZFpTVXR2V2tsNmFqQkZRWGROUkdGQlFYZGFVVWw0Q2tGUWFsTkhaR1JNU1haNVZVMUhTV3RhSzNVMlNtaEZPWEF4VG1wME0yUkZkSGRaYTAxNFptNUZWakpyTjAxSU1VSldiWGhuT1ZCelNtcHhlV05tYVNzS1pVRkpkMFJoUzI0eVEyUlBlRXR6ZUdObldVNXBORWgyYVVWdVduRjRiV1ZFZVc4eVdVWkpkSHB3U0daSlRWRnRZMUpUYkRreFZXVlBVME00SzFCMVJ3cG5kMDFMQ2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIiwic2lnIjoiVFVWVlEwbERWV2hCVm1WM1puZExiR3MxWmxaNmNGSkVWVkJvUlhjNVR6aEpNbkI0UXpWdVZHNVFabGxFUW5OUFFXbEZRVEJhUm5Gek9UbFJaMUk1YlVGMFJrMVhkRmR5VDJwdFZVTTBOM3BuWVc5dmJFdEpiMHhJTDA5M1pFMDkifV19LCJoYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZGNiNDkyNTljODY2MDdjMzQ2MzVkYWJiNDQzMWYwNjVlOWE3YTczNDcwNGNiNzNmMGFhMGY2YWFhMzg5NmEwNCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjY4ZTY1NmIyNTFlNjdlODM1OGJlZjg0ODNhYjBkNTFjNjYxOWYzZTdhMWE5ZjBlNzU4MzhkNDFmZjM2OGY3MjgifX19fQ==" + } + ], + "timestampVerificationData": { + "rfc3161Timestamps": [] + }, + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "MIICnzCCAiWgAwIBAgIUVHwehOtGn4KSD1H8RI581MfbyewwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjIxMTA4MjI1ODA2WhcNMjIxMTA4MjMwODA2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGg6Hjxt2UNiJ1kwwq5XQIIwMZnJfVQ3bF01uZKteMdcV/3qhCmWOecoxRqwrbYTshGg9NyXcBbve6zKwZVTLeqOCAUQwggFAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU7WpR60sCpgfu04wcsjvCFxt0fMkwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0RAQH/BBUwE4ERYnJpYW5AZGVoYW1lci5jb20wLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGEWXcR8AAABAMARjBEAiBRTrGE5Y1EnYniaJB+nsv89VaYx3QZjocEin3r91wfkAIgMss+fssu5SLQkn7WDTKXgow7SxbHYSZj3ykxArVnuzEwCgYIKoZIzj0EAwMDaAAwZQIxAPjSGddLIvyUMGIkZ+u6JhE9p1Njt3dEtwYkMxfnEV2k7MH1BVmxg9PsJjqycfi+eAIwDaKn2CdOxKsxcgYNi4HviEnZqxmeDyo2YFItzpHfIMQmcRSl91UeOSC8+PuGgwMK" + }, + { + "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" + } + ] + } + }, + "dsseEnvelope": { + "payload": "aGVsbG8sIHdvcmxkIQ==", + "payloadType": "text/plain", + "signatures": [ + { + "sig": "NEUCICUhAVewfwKlk5fVzpRDUPhEw9O8I2pxC5nTnPfYDBsOAiEA0ZFqs99QgR9mAtFMWtWrOjmUC47zgaoolKIoLH/OwdM=", + "keyid": "" + } + ] + } +} diff --git a/pkg/cmd/attestation/test/output.go b/pkg/cmd/attestation/test/output.go new file mode 100644 index 000000000..fff3decd3 --- /dev/null +++ b/pkg/cmd/attestation/test/output.go @@ -0,0 +1,16 @@ +package test + +import "os" + +func SuppressAndRestoreOutput() func() { + null, _ := os.Open(os.DevNull) + stdOut := os.Stdout + stdErr := os.Stderr + os.Stdout = null + os.Stderr = null + return func() { + defer null.Close() + os.Stdout = stdOut + os.Stderr = stdErr + } +} diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go new file mode 100644 index 000000000..eca86adf7 --- /dev/null +++ b/pkg/cmd/attestation/verification/attestation.go @@ -0,0 +1,112 @@ +package verification + +import ( + "bufio" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + "github.com/sigstore/sigstore-go/pkg/bundle" +) + +var ErrLocalAttestations = errors.New("failed to load local attestations") + +type FetchAttestationsConfig struct { + APIClient api.Client + BundlePath string + Digest string + Limit int + Owner string + Repo string +} + +func (c *FetchAttestationsConfig) IsBundleProvided() bool { + return c.BundlePath != "" +} + +func GetAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { + if c.IsBundleProvided() { + return GetLocalAttestations(c.BundlePath) + } + return getRemoteAttestations(c) +} + +// GetLocalAttestations returns a slice of attestations read from a local bundle file. +func GetLocalAttestations(path string) ([]*api.Attestation, error) { + fileExt := filepath.Ext(path) + switch fileExt { + case ".json": + attestations, err := loadBundleFromJSONFile(path) + if err != nil { + return nil, fmt.Errorf("bundle could not be loaded from JSON file: %w", err) + } + return attestations, nil + case ".jsonl": + attestations, err := loadBundlesFromJSONLinesFile(path) + if err != nil { + return nil, fmt.Errorf("bundles could not be loaded from JSON lines file: %w", err) + } + return attestations, nil + } + return nil, ErrLocalAttestations +} + +func loadBundleFromJSONFile(path string) ([]*api.Attestation, error) { + localAttestation, err := bundle.LoadJSONFromPath(path) + if err != nil { + return nil, err + } + + return []*api.Attestation{{Bundle: localAttestation}}, nil +} + +func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("could not open file: %w", err) + } + defer file.Close() + + attestations := []*api.Attestation{} + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + b := scanner.Bytes() + var bundle bundle.ProtobufBundle + bundle.Bundle = new(protobundle.Bundle) + err = bundle.UnmarshalJSON(b) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal bundle from JSON: %w", err) + } + a := api.Attestation{Bundle: &bundle} + attestations = append(attestations, &a) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return attestations, nil +} + +func getRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { + // check if Repo is set first because if Repo has been set, Owner will be set using the value of Repo. + // If Repo is not set, the field will remain empty. It will not be populated using the value of Owner. + if c.Repo != "" { + attestations, err := c.APIClient.GetByRepoAndDigest(c.Repo, c.Digest, c.Limit) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Repo, err) + } + return attestations, nil + } else if c.Owner != "" { + attestations, err := c.APIClient.GetByOwnerAndDigest(c.Owner, c.Digest, c.Limit) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Owner, err) + } + return attestations, nil + } + return nil, fmt.Errorf("owner or repo must be provided") +} diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go new file mode 100644 index 000000000..8c4cf34df --- /dev/null +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -0,0 +1,49 @@ +package verification + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadBundlesFromJSONLinesFile(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl" + attestations, err := loadBundlesFromJSONLinesFile(path) + + assert.NoError(t, err) + assert.Len(t, attestations, 2) +} + +func TestLoadBundleFromJSONFile(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0-bundle.json" + attestations, err := loadBundleFromJSONFile(path) + + assert.NoError(t, err) + assert.Len(t, attestations, 1) +} + +func TestGetLocalAttestations(t *testing.T) { + t.Run("with JSON file containing one bundle", func(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0-bundle.json" + attestations, err := GetLocalAttestations(path) + + assert.NoError(t, err) + assert.Len(t, attestations, 1) + }) + + t.Run("with JSON lines file containing multiple bundles", func(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl" + attestations, err := GetLocalAttestations(path) + + assert.NoError(t, err) + assert.Len(t, attestations, 2) + }) + + t.Run("with file with unrecognized extension", func(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0-bundles.tgz" + attestations, err := GetLocalAttestations(path) + + assert.ErrorIs(t, err, ErrLocalAttestations) + assert.Nil(t, attestations) + }) +} diff --git a/pkg/cmd/attestation/verification/embed/tuf-repo.github.com/root.json b/pkg/cmd/attestation/verification/embed/tuf-repo.github.com/root.json new file mode 100644 index 000000000..2990a2665 --- /dev/null +++ b/pkg/cmd/attestation/verification/embed/tuf-repo.github.com/root.json @@ -0,0 +1,163 @@ +{ + "signatures": [ + { + "keyid": "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", + "sig": "304402203c8f5f7443f7052923e82f9ca0b1bb61a33498444076a2f43e1285a47f6e562d022014de99a7e5413440896b6804944e3c49390cfe6e617211b8dc42a8e67675bc13" + }, + { + "keyid": "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", + "sig": "3044022009a20307f974af7e05cc9564eea497f45062e3b21272d1062713b3d22c868298022059d032ad973a28bdbd03959cf96b21398b6b6e2ca618c17ce6c13712246343a2" + }, + { + "keyid": "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a", + "sig": "3045022100edd270d36d0c8468b9a1f2ef1c81a270c72ffd50c65bca0ed1ebd424df09f64b022002b27ffafd7bc5bdfc25281b5b9b597cf2d67d4eeb4af2ff45eb3e666b860c21" + }, + { + "keyid": "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", + "sig": "30460221008d7d95434e576d5876b2db30fd645505ca546618bbc7a8e4b39f64e6a36df9ad022100c00a5294e4ddd02d48d28918b87a06bdfdeccd0618ecdcec29bb2597a05fe474" + }, + { + "keyid": "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", + "sig": "30450220215fb3d19d94560a3a2a6067a71c92daf867d13700c9500c4c32d8009a48a634022100df9fb6cee786313bf6c363daac4de39b3dd531f211f81d2391c41bd2d0f91a80" + }, + { + "keyid": "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", + "sig": "304502204091ac5e61b6462d262ecc8442781dd09843bed39942a95a4884c8c6a2c212ef022100dcee86392748f48950d04d539ac1a6643ed1f0b4bd6856f8aeb5a135826c846f" + }, + { + "keyid": "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", + "sig": "30460221009188548601a43b501223caeefca4876ae892e18a85c885004b4c8aeeb05a4421022100abdcc72d94597f8297d6297897ff96f285168dbe6b3d57f846dbc7a2948d2935" + }, + { + "keyid": "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", + "sig": "3046022100b440561545d48759dc4140cda9f8af7c9405a101d6136dd0a26edd6562b7064f022100cafa917ed90350494e47d226b64a8ec63ef5ceebb8ba4d2dec2ce018e4ad402a" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2024-06-23T08:29:18Z", + "keys": { + "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENki7aZVips5SgRzCd/Om0CGzQKY/\nnv84giqVDmdwb2ys82Z6soFLasvYYEEQcwqaC170n9gr93wHUgPc796uJA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@ashtom" + }, + "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElD0o2sOZN9n3RKQ7PtMLAoXj+2Ai\nn4PKT/pfnzDlNLrD3VTQwCc4sR4t+OLu4KQ+qk+kXkR9YuBsu3bdJZ1OWw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@nerdneha" + }, + "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC9RNAsuDCNO6T7qA7Y5F8orw2tIW\nr7rUr4ffxvzTMrbkVtjR/trtE0q0+T0zQ8TWLyI6EYMwb947ej2ItfkOyA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@jacobdepriest" + }, + "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBagkskNOpOTbetTX5CdnvMy+LiWn\nonRrNrqAHL4WgiebH7Uig7GLhC3bkeA/qgb926/vr9qhOPG9Buj2HatrPw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@gregose" + }, + "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7IEoVNwrprchXGhT5sAhSax7SOd3\n8duuISghCzfmHdKJWSbV2wJRamRiUVRtmA83K/qm5cT20WXMCT5QeM/D3A==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@trevrosen" + }, + "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC2wJ3xscyXxBLybJ9FVjwkyQMe53\nRHUz77AjMO8MzVaT8xw6ZvJqdNZiytYtigWULlINxw6frNsWJKb/f7lC8A==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@kommendorkapten" + }, + "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDdORwcruW3gqAgaLjH/nNdGMB4kQ\nAvA+wD6DyO4P/wR8ee2ce83NZHq1ZADKhve0rlYKaKy3CqyQ5SmlZ36Zhw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@krukow" + }, + "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENynVdQnM9h7xU71G7PiJpQaDemub\nkbjsjYwLlPJTQVuxQO8WeIpJf8MEh5rf01t2dDIuCsZ5gRx+QvDv0UzfsA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@mph4" + }, + "eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENKNcNcX+d73lS1TRFb9Vnp8JvOoh\nzYQ+in43iGenbG8RGo9L/6FJ2hoRbVU6xskvyuErcdPbCdI4GxrQ5i8hkw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-online-uri": "azurekms://production-tuf-root.vault.azure.net/keys/Online-Key/aaf375fd8ed24acb949a5cc173700b05" + } + }, + "roles": { + "root": { + "keyids": [ + "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", + "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", + "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", + "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", + "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", + "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", + "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", + "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a" + ], + "threshold": 3 + }, + "snapshot": { + "keyids": [ + "eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 21, + "x-tuf-on-ci-signing-period": 7 + }, + "targets": { + "keyids": [ + "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", + "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", + "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", + "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", + "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", + "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", + "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", + "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a" + ], + "threshold": 3 + }, + "timestamp": { + "keyids": [ + "eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 7, + "x-tuf-on-ci-signing-period": 6 + } + }, + "spec_version": "1.0.31", + "version": 1, + "x-tuf-on-ci-expiry-period": 240, + "x-tuf-on-ci-signing-period": 60 + } +} diff --git a/pkg/cmd/attestation/verification/policy.go b/pkg/cmd/attestation/verification/policy.go index eb384fe4e..91b54c75e 100644 --- a/pkg/cmd/attestation/verification/policy.go +++ b/pkg/cmd/attestation/verification/policy.go @@ -1,18 +1,20 @@ -package verify +package verification import ( "encoding/hex" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/sigstore/sigstore-go/pkg/verify" ) // BuildDigestPolicyOption builds a verify.ArtifactPolicyOption // from the given artifact digest and digest algorithm -func BuildDigestPolicyOption(artifactDigest, digestAlgorithm string) (verify.ArtifactPolicyOption, error) { +func BuildDigestPolicyOption(a artifact.DigestedArtifact) (verify.ArtifactPolicyOption, error) { // sigstore-go expects the artifact digest to be decoded from hex - decoded, err := hex.DecodeString(artifactDigest) + decoded, err := hex.DecodeString(a.Digest()) if err != nil { return nil, err } - return verify.WithArtifactDigest(digestAlgorithm, decoded), nil + return verify.WithArtifactDigest(a.Algorithm(), decoded), nil } diff --git a/pkg/cmd/attestation/verification/policy_test.go b/pkg/cmd/attestation/verification/policy_test.go deleted file mode 100644 index 121723428..000000000 --- a/pkg/cmd/attestation/verification/policy_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package verify - -import ( - "testing" - - "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" - "github.com/cli/cli/v2/pkg/cmd/attestation/github" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBuildPolicy(t *testing.T) { - opts := &VerifyOpts{ - ArtifactPath: "../../test/data/public-good/sigstore-js-2.1.0.tgz", - BundlePath: "../../test/data/public-good/sigstore-js-2.1.0-bundle.json", - GitHubClient: github.NewTestClient(), - OIDCIssuer: GitHubOIDCIssuer, - SAN: "fake san", - Owner: "github", - } - - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) - require.NoError(t, err) - - _, err = buildVerifyPolicy(opts, artifact.Digest()) - assert.NoError(t, err) -} diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index 824f276f8..f68821d6f 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -1,9 +1,10 @@ -package verify +package verification import ( "fmt" - "github.com/cli/cli/v2/pkg/cmd/attestation/github" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/logger" "github.com/sigstore/sigstore-go/pkg/bundle" "github.com/sigstore/sigstore-go/pkg/root" @@ -16,9 +17,20 @@ const ( GitHubIssuerOrg = "GitHub, Inc." ) +// AttestationProcessingResult captures processing a given attestation's signature verification and policy evaluation +type AttestationProcessingResult struct { + Attestation *api.Attestation + VerificationResult *verify.VerificationResult +} + +type SigstoreResults struct { + VerifyResults []*AttestationProcessingResult + Error error +} + type SigstoreConfig struct { CustomTrustedRoot string - Logger *output.Logger + Logger *logger.Logger NoPublicGood bool } @@ -28,7 +40,7 @@ type SigstoreVerifier struct { customVerifier *verify.SignedEntityVerifier policy verify.PolicyBuilder onlyVerifyWithGithub bool - Logger *output.Logger + Logger *logger.Logger } // NewSigstoreVerifier creates a new SigstoreVerifier struct @@ -91,7 +103,7 @@ func (v *SigstoreVerifier) chooseVerifier(b *bundle.ProtobufBundle) (*verify.Sig return nil, "", fmt.Errorf("leaf certificate issuer is not recognized") } -func (v *SigstoreVerifier) Verify(attestations []*github.Attestation) *SigstoreResults { +func (v *SigstoreVerifier) Verify(attestations []*api.Attestation) *SigstoreResults { // initialize the processing results before attempting to verify // with multiple verifiers results := make([]*AttestationProcessingResult, len(attestations)) diff --git a/pkg/cmd/attestation/verification/sigstore_test.go b/pkg/cmd/attestation/verification/sigstore_test.go index 4fb3e8e07..aa9e2235b 100644 --- a/pkg/cmd/attestation/verification/sigstore_test.go +++ b/pkg/cmd/attestation/verification/sigstore_test.go @@ -1,53 +1,49 @@ -package verify +package verification import ( "testing" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" - "github.com/cli/cli/v2/pkg/cmd/attestation/github" + "github.com/cli/cli/v2/pkg/cmd/attestation/logger" "github.com/cli/cli/v2/pkg/cmd/attestation/test" + "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var logger = output.NewDefaultLogger() - -var publicGoodOpts = Options{ - ArtifactPath: test.NormalizeRelativePath("../../test/data/public-good/sigstore-js-2.1.0.tgz"), - BundlePath: test.NormalizeRelativePath("../../test/data/public-good/sigstore-js-2.1.0-bundle.json"), - DigestAlgorithm: "sha512", - GitHubClient: github.NewTestClient(), - Logger: logger, - OIDCIssuer: GitHubOIDCIssuer, - Owner: "sigstore", - Limit: 30, - OCIClient: oci.NewMockClient(), - SANRegex: "^https://github.com/sigstore/", -} +var ( + artifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz") + bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json") + ociClient = oci.NewMockClient() +) func TestVerify_PublicGoodSuccess(t *testing.T) { - t.Skip() res := test.SuppressAndRestoreOutput() defer res() - artifact, err := artifact.NewDigestedArtifact(publicGoodOpts.OCIClient, publicGoodOpts.ArtifactPath, publicGoodOpts.DigestAlgorithm) + artifact, err := artifact.NewDigestedArtifact(ociClient, artifactPath, "sha512") require.NoError(t, err) - attestations, err := getAttestations(&publicGoodOpts, artifact.Digest()) - require.NoError(t, err) + c := FetchAttestationsConfig{ + APIClient: api.NewTestClient(), + BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), + Digest: artifact.DigestWithAlg(), + Owner: "sigstore", + } - policy, err := buildVerifyPolicy(&publicGoodOpts, artifact.Digest()) + attestations, err := GetAttestations(c) require.NoError(t, err) config := SigstoreConfig{ - CustomTrustedRoot: publicGoodOpts.CustomTrustedRoot, - Logger: publicGoodOpts.Logger, - NoPublicGood: publicGoodOpts.NoPublicGood, + Logger: logger.NewDefaultLogger(), + NoPublicGood: false, } - v, err := NewSigstoreVerifier(config, policy) + v, err := NewSigstoreVerifier(config, verify.PolicyBuilder{}) require.NoError(t, err) resp := v.Verify(attestations) @@ -62,32 +58,27 @@ func TestVerify_PublicGoodSuccess(t *testing.T) { } func TestVerify_PublicGoodFail_WithNoPublicGoodFlag(t *testing.T) { - t.Skip() res := test.SuppressAndRestoreOutput() defer res() - opts := publicGoodOpts - opts.NoPublicGood = true - - err := opts.AreFlagsValid() + artifact, err := artifact.NewDigestedArtifact(ociClient, artifactPath, "sha512") require.NoError(t, err) - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) - require.NoError(t, err) + c := FetchAttestationsConfig{ + BundlePath: bundlePath, + Digest: artifact.Digest(), + Owner: "sigstore", + } - attestations, err := getAttestations(&opts, artifact.Digest()) - require.NoError(t, err) - - policy, err := buildVerifyPolicy(&publicGoodOpts, artifact.Digest()) + attestations, err := GetAttestations(c) require.NoError(t, err) config := SigstoreConfig{ - CustomTrustedRoot: opts.CustomTrustedRoot, - Logger: opts.Logger, - NoPublicGood: opts.NoPublicGood, + Logger: logger.NewDefaultLogger(), + NoPublicGood: true, } - v, err := NewSigstoreVerifier(config, policy) + v, err := NewSigstoreVerifier(config, verify.PolicyBuilder{}) require.NoError(t, err) resp := v.Verify(attestations) @@ -99,28 +90,24 @@ func TestVerify_Failure(t *testing.T) { res := test.SuppressAndRestoreOutput() defer res() - opts := publicGoodOpts - opts.DigestAlgorithm = "sha256" - - err := opts.AreFlagsValid() + artifact, err := artifact.NewDigestedArtifact(ociClient, artifactPath, "sha512") require.NoError(t, err) - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) - require.NoError(t, err) + c := FetchAttestationsConfig{ + BundlePath: bundlePath, + Digest: artifact.Digest(), + Owner: "sigstore", + } - attestations, err := getAttestations(&opts, artifact.Digest()) - require.NoError(t, err) - - policy, err := buildVerifyPolicy(&opts, artifact.Digest()) + attestations, err := GetAttestations(c) require.NoError(t, err) config := SigstoreConfig{ - CustomTrustedRoot: opts.CustomTrustedRoot, - Logger: opts.Logger, - NoPublicGood: opts.NoPublicGood, + Logger: logger.NewDefaultLogger(), + NoPublicGood: true, } - v, err := NewSigstoreVerifier(config, policy) + v, err := NewSigstoreVerifier(config, verify.PolicyBuilder{}) require.NoError(t, err) resp := v.Verify(attestations) diff --git a/pkg/cmd/attestation/verification/tuf.go b/pkg/cmd/attestation/verification/tuf.go new file mode 100644 index 000000000..2f24ec119 --- /dev/null +++ b/pkg/cmd/attestation/verification/tuf.go @@ -0,0 +1,55 @@ +package verification + +import ( + "embed" + "fmt" + "os" + + "github.com/sigstore/sigstore-go/pkg/tuf" +) + +//go:embed embed +var embeddedRepos embed.FS + +const GitHubTUFMirror = "https://tuf-repo.github.com" + +// readEmbeddedRoot reads the embedded trust anchor for the given URL +func readEmbeddedRoot(url string) ([]byte, error) { + // the embed file system always uses forward slashes, even on Windows + p := fmt.Sprintf("embed/%s/root.json", tuf.URLToPath(url)) + + b, err := embeddedRepos.ReadFile(p) + if err != nil { + return nil, err + } + + return b, nil +} + +func DefaultOptionsWithCacheSetting() *tuf.Options { + opts := tuf.DefaultOptions() + + // The CODESPACES environment variable will be set to true in a Codespaces workspace + if os.Getenv("CODESPACES") == "true" { + // if the tool is being used in a Codespace, disable the local cache + // because there is a permissions issue preventing the tuf library + // from writing the Sigstore cache to the home directory + opts.DisableLocalCache = true + } + + return opts +} + +func GitHubTUFOptions() (*tuf.Options, error) { + opts := DefaultOptionsWithCacheSetting() + + // replace root and mirror url + root, err := readEmbeddedRoot(GitHubTUFMirror) + if err != nil { + return nil, err + } + opts.Root = root + opts.RepositoryBaseURL = GitHubTUFMirror + + return opts, nil +} diff --git a/pkg/cmd/attestation/verification/tuf_test.go b/pkg/cmd/attestation/verification/tuf_test.go new file mode 100644 index 000000000..89e52a7bc --- /dev/null +++ b/pkg/cmd/attestation/verification/tuf_test.go @@ -0,0 +1,18 @@ +package verification + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGitHubTUFOptions(t *testing.T) { + os.Setenv("CODESPACES", "true") + opts, err := GitHubTUFOptions() + assert.NoError(t, err) + + assert.Equal(t, GitHubTUFMirror, opts.RepositoryBaseURL) + assert.NotNil(t, opts.Root) + assert.True(t, opts.DisableLocalCache) +} diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index bffce4b52..19aa1c23e 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -7,15 +7,8 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/digest" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" - "github.com/cli/cli/v2/pkg/cmd/attestation/github" -) - -const ( - // OfflineMode is used when the user provides a bundle path - OfflineMode = "offline" - // OnlineMode is used when the user does not provide a bundle path - // An owner or repo scope is used to fetch attestations from GitHub - OnlineMode = "online" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/logger" ) // Options captures the options for the verify command @@ -34,35 +27,12 @@ type Options struct { SAN string SANRegex string Verbose bool - GitHubClient github.Client - Logger *output.Logger + APIClient api.Client + Logger *logger.Logger Limit int OCIClient oci.Client } -// Mode returns a string indicating either online or offline mode -func (opts *Options) Mode() string { - if opts.BundlePath == "" { - return OnlineMode - } - return OfflineMode -} - -// ConfigureGitHubClient configures a live GitHub client if the tool -// is running in online mode -func (opts *Options) ConfigureGitHubClient(version, date string) error { - if opts.Mode() == OfflineMode { - return nil - } - - client, err := github.NewLiveClient(version, date) - if err != nil { - return err - } - opts.GitHubClient = client - return nil -} - // ConfigureOCIClient configures an OCI client func (opts *Options) ConfigureOCIClient() { opts.OCIClient = oci.NewLiveClient() @@ -71,7 +41,7 @@ func (opts *Options) ConfigureOCIClient() { // ConfigureLogger configures a logger using configuration provided // through the options func (opts *Options) ConfigureLogger() { - opts.Logger = output.NewLogger(opts.Quiet, opts.Verbose) + opts.Logger = logger.NewLogger(opts.Quiet, opts.Verbose) } // Clean cleans the file path option values diff --git a/pkg/cmd/attestation/verify/options_test.go b/pkg/cmd/attestation/verify/options_test.go index 25fd3cb35..6caf6f625 100644 --- a/pkg/cmd/attestation/verify/options_test.go +++ b/pkg/cmd/attestation/verify/options_test.go @@ -9,8 +9,8 @@ import ( ) var ( - publicGoodArtifactPath = test.NormalizeRelativePath("../../test/data/public-good/sigstore-js-2.1.0.tgz") - publicGoodBundlePath = test.NormalizeRelativePath("../../test/data/public-good/sigstore-js-2.1.0-bundle.json") + publicGoodArtifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz") + publicGoodBundlePath = test.NormalizeRelativePath("../test/data/psigstore-js-2.1.0-bundle.json") ) func TestAreFlagsValid(t *testing.T) { @@ -195,35 +195,3 @@ func TestClean(t *testing.T) { opts.Clean() assert.Equal(t, validBundlePath, opts.BundlePath) } - -func TestMode(t *testing.T) { - t.Run("run in offline mode when bundle is provided", func(t *testing.T) { - opts := Options{ - ArtifactPath: publicGoodArtifactPath, - BundlePath: publicGoodBundlePath, - } - - assert.Equal(t, OfflineMode, opts.Mode()) - }) - - t.Run("run in offline mode when bundle and repo are provided", func(t *testing.T) { - opts := &Options{ - ArtifactPath: publicGoodArtifactPath, - BundlePath: publicGoodBundlePath, - DigestAlgorithm: "sha512", - Repo: "sigstore/sigstore-js", - } - - assert.Equal(t, OfflineMode, opts.Mode()) - }) - - t.Run("run in online mode when repo are provided", func(t *testing.T) { - opts := &Options{ - ArtifactPath: publicGoodArtifactPath, - DigestAlgorithm: "sha512", - Repo: "sigstore/sigstore-js", - } - - assert.Equal(t, OnlineMode, opts.Mode()) - }) -} diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 4b31c154e..6967ff222 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -6,6 +6,7 @@ import ( "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" ) @@ -16,19 +17,20 @@ const ( GitHubRunner = "github-hosted" ) -func buildSANMatcher(opts *Options) (verify.SubjectAlternativeNameMatcher, error) { - if opts.SAN != "" || opts.SANRegex != "" { - sanMatcher, err := verify.NewSANMatcher(opts.SAN, "", opts.SANRegex) - if err != nil { - return verify.SubjectAlternativeNameMatcher{}, err - } - return sanMatcher, nil +func buildSANMatcher(san, sanRegex string) (verify.SubjectAlternativeNameMatcher, error) { + if san == "" && sanRegex == "" { + return verify.SubjectAlternativeNameMatcher{}, nil } - return verify.SubjectAlternativeNameMatcher{}, nil + + sanMatcher, err := verify.NewSANMatcher(san, "", sanRegex) + if err != nil { + return verify.SubjectAlternativeNameMatcher{}, err + } + return sanMatcher, nil } func buildCertificateIdentityOption(opts *Options, runnerEnv string) (verify.PolicyOption, error) { - sanMatcher, err := buildSANMatcher(opts) + sanMatcher, err := buildSANMatcher(opts.SAN, opts.SANRegex) if err != nil { return nil, err } @@ -67,8 +69,8 @@ func buildVerifyCertIdOption(opts *Options) (verify.PolicyOption, error) { return withAnyRunner, nil } -func buildVerifyPolicy(opts *Options, artifactDigest string) (verify.PolicyBuilder, error) { - artifactDigestPolicyOption, err := verification.BuildDigestPolicyOption(artifactDigest, opts.DigestAlgorithm) +func buildVerifyPolicy(opts *Options, a artifact.DigestedArtifact) (verify.PolicyBuilder, error) { + artifactDigestPolicyOption, err := verification.BuildDigestPolicyOption(a) if err != nil { return verify.PolicyBuilder{}, err } diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go new file mode 100644 index 000000000..a7e3faf22 --- /dev/null +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -0,0 +1,29 @@ +package verify + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildPolicy(t *testing.T) { + ociClient := oci.NewMockClient() + artifactPath := "../test/data/sigstore-js-2.1.0.tgz" + digestAlg := "sha256" + + artifact, err := artifact.NewDigestedArtifact(ociClient, artifactPath, digestAlg) + require.NoError(t, err) + + opts := &Options{ + ArtifactPath: artifactPath, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + } + + _, err = buildVerifyPolicy(opts, *artifact) + assert.NoError(t, err) +} diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index f94dd4b63..3a9a47f41 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -1,17 +1,25 @@ package verify import ( + "encoding/json" + "errors" + "fmt" "os" - "github.com/cli/cli/v2/api" - "github.com/cli/cli/v2/pkg/cmd/attestation/github" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/logger" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" ) +var ErrNoMatchingSLSAPredicate = fmt.Errorf("the attestation does not have the expected SLSA predicate type: %s", SLSAPredicateType) + func NewVerifyCmd(f *cmdutil.Factory) *cobra.Command { - opts := &verify.Options{} + opts := &Options{} verifyCmd := &cobra.Command{ Use: "verify ", Args: cobra.ExactArgs(1), @@ -60,6 +68,8 @@ func NewVerifyCmd(f *cmdutil.Factory) *cobra.Command { // If an error is returned, its message will be printed to the terminal // along with information about how use the command PreRunE: func(cmd *cobra.Command, args []string) error { + opts.APIClient = api.NewLiveClient() + // Create a logger for use throughout the verify command opts.ConfigureLogger() @@ -86,7 +96,7 @@ func NewVerifyCmd(f *cmdutil.Factory) *cobra.Command { // when RunE is used, the command usage will be printed // We only want to print the error, not usage Run: func(cmd *cobra.Command, args []string) { - if err := runVerify(opts); err != nil { + if err := RunVerify(opts); err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Redf("Failed to verify the artifact: %s", err.Error())) os.Exit(1) } @@ -99,24 +109,25 @@ func NewVerifyCmd(f *cmdutil.Factory) *cobra.Command { verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by") verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") verifyCmd.MarkFlagsMutuallyExclusive("owner", "repo") + verifyCmd.MarkFlagsOneRequired("owner", "repo") verifyCmd.Flags().BoolVarP(&opts.NoPublicGood, "no-public-good", "", false, "Only verify attestations signed with GitHub's Sigstore instance") verifyCmd.Flags().BoolVarP(&opts.JsonResult, "json-result", "j", false, "Output verification result as JSON lines") verifyCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "If set to true, the CLI will not print any diagnostic logging.") verifyCmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "If set to true, the CLI will print verbose diagnostic logging.") verifyCmd.MarkFlagsMutuallyExclusive("quiet", "verbose") verifyCmd.Flags().StringVarP(&opts.CustomTrustedRoot, "custom-trusted-root", "", "", "Path to a custom trustedroot.json file to use for verification") - verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", github.DefaultLimit, "Maximum number of attestations to fetch") + verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch") // policy enforcement flags verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners.") verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's subject alternative name matches the provided value exactly") verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's subject alternative name matches the provided regex") verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex") - verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verify.GitHubOIDCIssuer, "Issuer of the OIDC token") + verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", GitHubOIDCIssuer, "Issuer of the OIDC token") return verifyCmd } -func runVerify(opts *Options) error { +func RunVerify(opts *Options) error { artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) if err != nil { return fmt.Errorf("failed to digest artifact: %s", err) @@ -124,26 +135,33 @@ func runVerify(opts *Options) error { opts.Logger.Printf("Verifying attestations for the artifact found at %s\n", artifact.URL) - attestations, err := getAttestations(opts, artifact.DigestWithAlg()) + c := verification.FetchAttestationsConfig{ + APIClient: opts.APIClient, + Digest: artifact.DigestWithAlg(), + Limit: opts.Limit, + Owner: opts.Owner, + Repo: opts.Repo, + } + attestations, err := verification.GetAttestations(c) if err != nil { - if ok := errors.Is(err, github.ErrNoAttestations{}); ok { + if ok := errors.Is(err, api.ErrNoAttestations{}); ok { return fmt.Errorf("no attestations found for subject: %s", artifact.DigestWithAlg()) } return fmt.Errorf("failed to fetch attestations for subject: %s", artifact.DigestWithAlg()) } - policy, err := buildVerifyPolicy(opts, artifact.Digest()) + policy, err := buildVerifyPolicy(opts, *artifact) if err != nil { return fmt.Errorf("failed to build policy: %w", err) } - config := SigstoreConfig{ + config := verification.SigstoreConfig{ CustomTrustedRoot: opts.CustomTrustedRoot, Logger: opts.Logger, NoPublicGood: opts.NoPublicGood, } - sv, err := NewSigstoreVerifier(config, policy) + sv, err := verification.NewSigstoreVerifier(config, policy) if err != nil { return err } @@ -189,7 +207,7 @@ func runVerify(opts *Options) error { return nil } -func verifySLSAPredicateType(logger *output.Logger, apr []*AttestationProcessingResult) error { +func verifySLSAPredicateType(logger *logger.Logger, apr []*verification.AttestationProcessingResult) error { logger.VerbosePrintf("Evaluating attestations have valid SLSA predicate type...\n") for _, result := range apr { diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go new file mode 100644 index 000000000..e263ddffe --- /dev/null +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -0,0 +1,182 @@ +package verify + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/logger" + "github.com/cli/cli/v2/pkg/cmd/attestation/test" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/sigstore/sigstore-go/pkg/verify" + + "github.com/stretchr/testify/assert" +) + +const ( + SigstoreSanValue = "https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main" + SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/" +) + +func TestRunVerify(t *testing.T) { + res := test.SuppressAndRestoreOutput() + defer res() + + logger := logger.NewDefaultLogger() + + publicGoodOpts := Options{ + ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"), + BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), + DigestAlgorithm: "sha512", + APIClient: api.NewTestClient(), + Logger: logger, + OCIClient: oci.NewMockClient(), + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + } + + t.Run("with valid artifact and bundle", func(t *testing.T) { + assert.Nil(t, RunVerify(&publicGoodOpts)) + }) + + t.Run("with failing OCI artifact fetch", func(t *testing.T) { + opts := publicGoodOpts + opts.ArtifactPath = "oci://ghcr.io/github/test" + opts.OCIClient = oci.NewReferenceFailClient() + + err := RunVerify(&opts) + assert.Error(t, err) + assert.ErrorContains(t, err, "failed to digest artifact") + }) + + t.Run("with missing artifact path", func(t *testing.T) { + opts := publicGoodOpts + opts.ArtifactPath = "../test/data/non-existent-artifact.zip" + assert.Error(t, RunVerify(&opts)) + }) + + t.Run("with missing bundle path", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "../test/data/non-existent-sigstoreBundle.json" + assert.Error(t, RunVerify(&opts)) + }) + + t.Run("with invalid signature", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "../test/data/sigstoreBundle-invalid-signature.json" + + err := RunVerify(&opts) + assert.Error(t, err) + assert.ErrorContains(t, err, "at least one attestation failed to verify") + assert.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\"") + }) + + t.Run("with owner", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "" + opts.Owner = "github" + + assert.Nil(t, RunVerify(&opts)) + }) + + t.Run("with repo", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "" + opts.Repo = "github/example" + + assert.Nil(t, RunVerify(&opts)) + }) + + t.Run("with invalid repo", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "" + opts.Repo = "wrong/example" + opts.APIClient = api.NewFailTestClient() + + err := RunVerify(&opts) + assert.Error(t, err) + assert.ErrorContains(t, err, "failed to fetch attestations for subject") + }) + + t.Run("with invalid owner", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "" + opts.APIClient = api.NewFailTestClient() + opts.Owner = "wrong-owner" + + err := RunVerify(&opts) + assert.Error(t, err) + assert.ErrorContains(t, err, "failed to fetch attestations for subject") + }) + + t.Run("with invalid OIDC issuer", func(t *testing.T) { + opts := publicGoodOpts + opts.OIDCIssuer = "not-a-real-issuer" + assert.Error(t, RunVerify(&opts)) + }) + + t.Run("with SAN enforcement", func(t *testing.T) { + opts := Options{ + ArtifactPath: "../test/data/sigstore-js-2.1.0.tgz", + BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json", + APIClient: api.NewTestClient(), + DigestAlgorithm: "sha512", + Logger: logger, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SAN: SigstoreSanValue, + } + assert.Nil(t, RunVerify(&opts)) + }) + + t.Run("with invalid SAN", func(t *testing.T) { + opts := publicGoodOpts + opts.SAN = "fake san" + assert.Error(t, RunVerify(&opts)) + }) + + t.Run("with SAN regex enforcement", func(t *testing.T) { + opts := publicGoodOpts + opts.SANRegex = SigstoreSanRegex + assert.Nil(t, RunVerify(&opts)) + }) + + t.Run("with invalid SAN regex", func(t *testing.T) { + opts := publicGoodOpts + opts.SANRegex = "^https://github.com/sigstore/not-real/" + assert.Error(t, RunVerify(&opts)) + }) + + t.Run("with no matching OIDC issuer", func(t *testing.T) { + opts := publicGoodOpts + opts.OIDCIssuer = "some-other-issuer" + + assert.Error(t, RunVerify(&opts)) + }) + + t.Run("with valid artifact and JSON lines file containing multiple Sigstore bundles", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl" + assert.Nil(t, RunVerify(&opts)) + }) +} + +func TestVerifySLSAPredicateType_InvalidPredicate(t *testing.T) { + statement := &in_toto.Statement{} + statement.PredicateType = "some-other-predicate-type" + + apr := []*verification.AttestationProcessingResult{ + { + VerificationResult: &verify.VerificationResult{ + Statement: statement, + }, + }, + } + + err := verifySLSAPredicateType(logger.NewDefaultLogger(), apr) + assert.Error(t, err) + assert.ErrorIs(t, err, ErrNoMatchingSLSAPredicate) +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index b97dc09f0..b9600145f 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -125,7 +125,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command, cmd.AddCommand(actionsCmd.NewCmdActions(f)) cmd.AddCommand(aliasCmd.NewCmdAlias(f)) cmd.AddCommand(authCmd.NewCmdAuth(f)) - cmd.AddCommand(attestationCmd.NewCmdAttestation(f.IOStreams, version, buildDate)) + cmd.AddCommand(attestationCmd.NewCmdAttestation(f)) cmd.AddCommand(configCmd.NewCmdConfig(f)) cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil)) cmd.AddCommand(gistCmd.NewCmdGist(f))