add additional packages to support verify command

Signed-off-by: Meredith Lancaster <malancas@github.com>
This commit is contained in:
Meredith Lancaster 2024-03-04 08:25:17 -07:00
parent 82bd13b127
commit 501add44c0
31 changed files with 1477 additions and 210 deletions

View file

@ -1,4 +1,4 @@
package github
package api
import (
"fmt"

View file

@ -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
}

View file

@ -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)
}

View file

@ -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,
}
}

View file

@ -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")
}

View file

@ -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)

View file

@ -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"
)

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -0,0 +1,61 @@
{
"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1",
"verificationMaterial": {
"x509CertificateChain": {
"certificates": [
{
"rawBytes": "MIIGtDCCBjugAwIBAgIUCJLipSt09KLFc0JYfuDrSan//LswCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwODI5MTU0MDIzWhcNMjMwODI5MTU1MDIzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPfm6LPXQeJTC89UOqiNWmnZYGmX4T3iLZGi0EV4bfOoM86Hza94XqyuwxAoWpCPecFCEbAe8l2dg/er3O9LEFqOCBVowggVWMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUeqpCXHr3pcUaL3EFKR+KsmuKQqowHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wYwYDVR0RAQH/BFkwV4ZVaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMBIGCisGAQQBg78wAQIEBHB1c2gwNgYKKwYBBAGDvzABAwQoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAVBgorBgEEAYO/MAEEBAdSZWxlYXNlMCIGCisGAQQBg78wAQUEFHNpZ3N0b3JlL3NpZ3N0b3JlLWpzMB0GCisGAQQBg78wAQYED3JlZnMvaGVhZHMvbWFpbjA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wZQYKKwYBBAGDvzABCQRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwNwYKKwYBBAGDvzABDAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMwOAYKKwYBBAGDvzABDQQqDCgyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0MB8GCisGAQQBg78wAQ4EEQwPcmVmcy9oZWFkcy9tYWluMBkGCisGAQQBg78wAQ8ECwwJNDk1NTc0NTU1MCsGCisGAQQBg78wARAEHQwbaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlMBgGCisGAQQBg78wAREECgwINzEwOTYzNTMwZQYKKwYBBAGDvzABEgRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAUBgorBgEEAYO/MAEUBAYMBHB1c2gwWgYKKwYBBAGDvzABFQRMDEpodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvYWN0aW9ucy9ydW5zLzYwMTQ0ODg2NjYvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABikHz+vwAAAQDAEcwRQIgZEo8c0eCZHEh4uzzJzFz9T+EfSTNTtB2FIH18vXpkOsCIQDE1MTti9RoDnRO3SET1Zkad6FoTx/k6ztQcwIDPmnRxTAKBggqhkjOPQQDAwNnADBkAjBB06fmNXx6ToaClFg2kOxnfLGgrvoR3F5GjDtvDBB8m9SWQNzL211jYmS/g+YbbyUCMC+ad6jIK+efe4XOIlhLcWxeZbBtMjKSrPxmm4jR3BFQQOBR+8r27CioyhxoSqvLuw=="
}
]
},
"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": ""
}
]
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -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": ""
}
]
}
}

View file

@ -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": ""
}
]
}
}

View file

@ -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
}
}

View file

@ -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")
}

View file

@ -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)
})
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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))

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}

View file

@ -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

View file

@ -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())
})
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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 <artifact-path-or-url>",
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 <owner>/<repo>")
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 {

View file

@ -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)
}

View file

@ -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))