Merge pull request #9542 from kommendorkapten/policy-tenancy

Added tenancy aware attestation commands
This commit is contained in:
Fredrik Skogman 2024-09-11 11:53:30 +02:00 committed by GitHub
commit aa931c5aa7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 550 additions and 153 deletions

3
.gitignore vendored
View file

@ -27,4 +27,7 @@
# vim
*.swp
# Emacs
*~
vendor/

View file

@ -8,7 +8,6 @@ import (
"github.com/cli/cli/v2/api"
ioconfig "github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/go-gh/v2/pkg/auth"
)
const (
@ -18,12 +17,14 @@ const (
)
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)
GetTrustDomain() (string, error)
}
type LiveClient struct {
@ -32,9 +33,7 @@ type LiveClient struct {
logger *ioconfig.Handler
}
func NewLiveClient(hc *http.Client, l *ioconfig.Handler) *LiveClient {
host, _ := auth.DefaultHost()
func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClient {
return &LiveClient{
api: api.NewClientFromHTTP(hc),
host: strings.TrimSuffix(host, "/"),
@ -64,6 +63,12 @@ func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*At
return c.getAttestations(url, owner, digest, limit)
}
// GetTrustDomain returns the current trust domain. If the default is used
// the empty string is returned
func (c *LiveClient) GetTrustDomain() (string, error) {
return c.getTrustDomain(MetaPath)
}
func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) {
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest)
@ -102,3 +107,14 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At
return attestations, nil
}
func (c *LiveClient) getTrustDomain(url string) (string, error) {
var resp MetaResponse
err := c.api.REST(c.host, http.MethodGet, url, nil, &resp)
if err != nil {
return "", err
}
return resp.Domains.ArtifactAttestations.TrustDomain, nil
}

View file

@ -172,3 +172,35 @@ func TestGetByDigest_Error(t *testing.T) {
require.Error(t, err)
require.Nil(t, attestations)
}
func TestGetTrustDomain(t *testing.T) {
fetcher := mockMetaGenerator{
TrustDomain: "foo",
}
t.Run("with returned trust domain", func(t *testing.T) {
c := LiveClient{
api: mockAPIClient{
OnREST: fetcher.OnREST,
},
logger: io.NewTestHandler(),
}
td, err := c.GetTrustDomain()
require.Nil(t, err)
require.Equal(t, "foo", td)
})
t.Run("with error", func(t *testing.T) {
c := LiveClient{
api: mockAPIClient{
OnREST: fetcher.OnRESTError,
},
logger: io.NewTestHandler(),
}
td, err := c.GetTrustDomain()
require.Equal(t, "", td)
require.ErrorContains(t, err, "test error")
})
}

View file

@ -10,12 +10,17 @@ import (
type mockAPIClient struct {
OnRESTWithNext func(hostname, method, p string, body io.Reader, data interface{}) (string, error)
OnREST func(hostname, method, p string, body io.Reader, data interface{}) error
}
func (m mockAPIClient) RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
return m.OnRESTWithNext(hostname, method, p, body, data)
}
func (m mockAPIClient) REST(hostname, method, p string, body io.Reader, data interface{}) error {
return m.OnREST(hostname, method, p, body, data)
}
type mockDataGenerator struct {
NumAttestations int
}
@ -87,3 +92,26 @@ func (m mockDataGenerator) OnRESTWithNextNoAttestations(hostname, method, p stri
func (m mockDataGenerator) OnRESTWithNextError(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
return "", errors.New("failed to get attestations")
}
type mockMetaGenerator struct {
TrustDomain string
}
func (m mockMetaGenerator) OnREST(hostname, method, p string, body io.Reader, data interface{}) error {
var template = `
{
"domains": {
"artifact_attestations": {
"trust_domain": "%s"
}
}
}
`
var jsonString = fmt.Sprintf(template, m.TrustDomain)
return json.Unmarshal([]byte(jsonString), &data)
}
func (m mockMetaGenerator) OnRESTError(hostname, method, p string, body io.Reader, data interface{}) error {
return errors.New("test error")
}

View file

@ -9,6 +9,7 @@ import (
type MockClient struct {
OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error)
OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error)
OnGetTrustDomain func() (string, error)
}
func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) {
@ -19,6 +20,10 @@ func (m MockClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Att
return m.OnGetByOwnerAndDigest(owner, digest, limit)
}
func (m MockClient) GetTrustDomain() (string, error) {
return m.OnGetTrustDomain()
}
func makeTestAttestation() Attestation {
return Attestation{Bundle: data.SigstoreBundle(nil)}
}

View file

@ -0,0 +1,15 @@
package api
const MetaPath = "meta"
type ArtifactAttestations struct {
TrustDomain string `json:"trust_domain"`
}
type Domain struct {
ArtifactAttestations ArtifactAttestations `json:"artifact_attestations"`
}
type MetaResponse struct {
Domains Domain `json:"domains"`
}

View file

@ -4,14 +4,11 @@ import (
"errors"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/go-gh/v2/pkg/auth"
)
var ErrUnsupportedHost = errors.New("An unsupported host was detected. Note that gh attestation does not currently support GHES")
func IsHostSupported() error {
host, _ := auth.DefaultHost()
func IsHostSupported(host string) error {
// Note that this check is slightly redundant as Tenancy should not be considered Enterprise
// but the ghinstance package has not been updated to reflect this yet.
if ghinstance.IsEnterprise(host) && !ghinstance.IsTenancy(host) {

View file

@ -3,6 +3,8 @@ package auth
import (
"testing"
ghauth "github.com/cli/go-gh/v2/pkg/auth"
"github.com/stretchr/testify/require"
)
@ -38,7 +40,8 @@ func TestIsHostSupported(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("GH_HOST", tc.host)
err := IsHostSupported()
host, _ := ghauth.DefaultHost()
err := IsHostSupported(host)
if tc.expectedErr {
require.ErrorIs(t, err, ErrUnsupportedHost)
} else {

View file

@ -11,6 +11,7 @@ import (
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
ghauth "github.com/cli/go-gh/v2/pkg/auth"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
@ -78,16 +79,18 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman
if err != nil {
return err
}
opts.APIClient = api.NewLiveClient(hc, opts.Logger)
opts.OCIClient = oci.NewLiveClient()
opts.Store = NewLiveStore("")
if err := auth.IsHostSupported(); err != nil {
if opts.Hostname == "" {
opts.Hostname, _ = ghauth.DefaultHost()
}
if err := auth.IsHostSupported(opts.Hostname); err != nil {
return err
}
opts.APIClient = api.NewLiveClient(hc, opts.Hostname, opts.Logger)
opts.OCIClient = oci.NewLiveClient()
opts.Store = NewLiveStore("")
if runF != nil {
return runF(opts)
}
@ -106,6 +109,7 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman
downloadCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", "", "Filter attestations by provided predicate type")
cmdutil.StringEnumFlag(downloadCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
downloadCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch")
downloadCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")
return downloadCmd
}

View file

@ -24,6 +24,7 @@ type Options struct {
Owner string
PredicateType string
Repo string
Hostname string
}
func (opts *Options) AreFlagsValid() error {

View file

@ -55,17 +55,27 @@ type AttestationDetail struct {
WorkflowID string `json:"workflowId"`
}
func getOrgAndRepo(repoURL string) (string, string, error) {
after, found := strings.CutPrefix(repoURL, "https://github.com/")
if !found {
return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL)
func getOrgAndRepo(tenant, repoURL string) (string, string, error) {
var after string
var found bool
if tenant == "" {
after, found = strings.CutPrefix(repoURL, "https://github.com/")
if !found {
return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL)
}
} else {
after, found = strings.CutPrefix(repoURL,
fmt.Sprintf("https://%s.ghe.com/", tenant))
if !found {
return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL)
}
}
parts := strings.Split(after, "/")
return parts[0], parts[1], nil
}
func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) {
func getAttestationDetail(tenant string, attr api.Attestation) (AttestationDetail, error) {
envelope, err := attr.Bundle.Envelope()
if err != nil {
return AttestationDetail{}, fmt.Errorf("failed to get envelope from bundle: %v", err)
@ -87,7 +97,7 @@ func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) {
return AttestationDetail{}, fmt.Errorf("failed to unmarshal predicate: %v", err)
}
org, repo, err := getOrgAndRepo(predicate.BuildDefinition.ExternalParameters.Workflow.Repository)
org, repo, err := getOrgAndRepo(tenant, predicate.BuildDefinition.ExternalParameters.Workflow.Repository)
if err != nil {
return AttestationDetail{}, fmt.Errorf("failed to parse attestation content: %v", err)
}
@ -101,11 +111,11 @@ func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) {
}, nil
}
func getDetailsAsSlice(results []*verification.AttestationProcessingResult) ([][]string, error) {
func getDetailsAsSlice(tenant string, results []*verification.AttestationProcessingResult) ([][]string, error) {
details := make([][]string, len(results))
for i, result := range results {
detail, err := getAttestationDetail(*result.Attestation)
detail, err := getAttestationDetail(tenant, *result.Attestation)
if err != nil {
return nil, fmt.Errorf("failed to get attestation detail: %v", err)
}
@ -114,11 +124,11 @@ func getDetailsAsSlice(results []*verification.AttestationProcessingResult) ([][
return details, nil
}
func getAttestationDetails(results []*verification.AttestationProcessingResult) ([]AttestationDetail, error) {
func getAttestationDetails(tenant string, results []*verification.AttestationProcessingResult) ([]AttestationDetail, error) {
details := make([]AttestationDetail, len(results))
for i, result := range results {
detail, err := getAttestationDetail(*result.Attestation)
detail, err := getAttestationDetail(tenant, *result.Attestation)
if err != nil {
return nil, fmt.Errorf("failed to get attestation detail: %v", err)
}

View file

@ -12,7 +12,7 @@ import (
func TestGetOrgAndRepo(t *testing.T) {
t.Run("with valid source URL", func(t *testing.T) {
sourceURL := "https://github.com/github/gh-attestation"
org, repo, err := getOrgAndRepo(sourceURL)
org, repo, err := getOrgAndRepo("", sourceURL)
require.Nil(t, err)
require.Equal(t, "github", org)
require.Equal(t, "gh-attestation", repo)
@ -20,11 +20,19 @@ func TestGetOrgAndRepo(t *testing.T) {
t.Run("with invalid source URL", func(t *testing.T) {
sourceURL := "hub.com/github/gh-attestation"
org, repo, err := getOrgAndRepo(sourceURL)
org, repo, err := getOrgAndRepo("", sourceURL)
require.Error(t, err)
require.Zero(t, org)
require.Zero(t, repo)
})
t.Run("with valid source tenant URL", func(t *testing.T) {
sourceURL := "https://foo.ghe.com/github/gh-attestation"
org, repo, err := getOrgAndRepo("foo", sourceURL)
require.Nil(t, err)
require.Equal(t, "github", org)
require.Equal(t, "gh-attestation", repo)
})
}
func TestGetAttestationDetail(t *testing.T) {
@ -35,7 +43,7 @@ func TestGetAttestationDetail(t *testing.T) {
require.NoError(t, err)
attestation := attestations[0]
detail, err := getAttestationDetail(*attestation)
detail, err := getAttestationDetail("", *attestation)
require.NoError(t, err)
require.Equal(t, "sigstore", detail.OrgName)

View file

@ -3,13 +3,16 @@ package inspect
import (
"fmt"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/tableprinter"
"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/auth"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
ghauth "github.com/cli/go-gh/v2/pkg/auth"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
@ -66,8 +69,11 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
},
RunE: func(cmd *cobra.Command, args []string) error {
opts.OCIClient = oci.NewLiveClient()
if opts.Hostname == "" {
opts.Hostname, _ = ghauth.DefaultHost()
}
if err := auth.IsHostSupported(); err != nil {
if err := auth.IsHostSupported(opts.Hostname); err != nil {
return err
}
@ -78,6 +84,26 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
config := verification.SigstoreConfig{
Logger: opts.Logger,
}
// Prepare for tenancy if detected
if ghinstance.IsTenancy(opts.Hostname) {
hc, err := f.HttpClient()
if err != nil {
return err
}
apiClient := api.NewLiveClient(hc, opts.Hostname, opts.Logger)
td, err := apiClient.GetTrustDomain()
if err != nil {
return err
}
tenant, found := ghinstance.TenantName(opts.Hostname)
if !found {
return fmt.Errorf("Invalid hostname provided: '%s'",
opts.Hostname)
}
config.TrustDomain = td
opts.Tenant = tenant
}
opts.SigstoreVerifier = verification.NewLiveSigstoreVerifier(config)
@ -90,6 +116,7 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
inspectCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles")
inspectCmd.MarkFlagRequired("bundle") //nolint:errcheck
inspectCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")
cmdutil.StringEnumFlag(inspectCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
cmdutil.AddFormatFlags(inspectCmd, &opts.exporter)
@ -125,7 +152,7 @@ func runInspect(opts *Options) error {
// If the user provides the --format=json flag, print the results in JSON format
if opts.exporter != nil {
details, err := getAttestationDetails(res.VerifyResults)
details, err := getAttestationDetails(opts.Tenant, res.VerifyResults)
if err != nil {
return fmt.Errorf("failed to get attestation detail: %v", err)
}
@ -138,7 +165,7 @@ func runInspect(opts *Options) error {
}
// otherwise, print results in a table
details, err := getDetailsAsSlice(res.VerifyResults)
details, err := getDetailsAsSlice(opts.Tenant, res.VerifyResults)
if err != nil {
return fmt.Errorf("failed to parse attestation details: %v", err)
}

View file

@ -18,6 +18,8 @@ type Options struct {
OCIClient oci.Client
SigstoreVerifier verification.SigstoreVerifier
exporter cmdutil.Exporter
Hostname string
Tenant string
}
// Clean cleans the file path option values

View file

@ -6,9 +6,13 @@ import (
"fmt"
"os"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
ghauth "github.com/cli/go-gh/v2/pkg/auth"
"github.com/MakeNowJust/heredoc"
"github.com/sigstore/sigstore-go/pkg/tuf"
@ -19,6 +23,8 @@ type Options struct {
TufUrl string
TufRootPath string
VerifyOnly bool
Hostname string
TrustDomain string
}
type tufClientInstantiator func(o *tuf.Options) (*tuf.Client, error)
@ -54,10 +60,28 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
gh attestation trusted-root
`),
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.IsHostSupported(); err != nil {
if opts.Hostname == "" {
opts.Hostname, _ = ghauth.DefaultHost()
}
if err := auth.IsHostSupported(opts.Hostname); err != nil {
return err
}
if ghinstance.IsTenancy(opts.Hostname) {
hc, err := f.HttpClient()
if err != nil {
return err
}
logger := io.NewHandler(f.IOStreams)
apiClient := api.NewLiveClient(hc, opts.Hostname, logger)
td, err := apiClient.GetTrustDomain()
if err != nil {
return err
}
opts.TrustDomain = td
}
if runF != nil {
return runF(opts)
}
@ -74,12 +98,19 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
trustedRootCmd.Flags().StringVarP(&opts.TufRootPath, "tuf-root", "", "", "Path to the TUF root.json file on disk")
trustedRootCmd.MarkFlagsRequiredTogether("tuf-url", "tuf-root")
trustedRootCmd.Flags().BoolVarP(&opts.VerifyOnly, "verify-only", "", false, "Don't output trusted_root.jsonl contents")
trustedRootCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")
return &trustedRootCmd
}
type tufConfig struct {
tufOptions *tuf.Options
targets []string
}
func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
var tufOptions []*tuf.Options
var tufOptions []tufConfig
var defaultTR = "trusted_root.json"
tufOpt := verification.DefaultOptionsWithCacheSetting()
// Disable local caching, so we get up-to-date response from TUF repository
@ -93,37 +124,54 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
tufOpt.Root = tufRoot
tufOpt.RepositoryBaseURL = opts.TufUrl
tufOptions = append(tufOptions, tufOpt)
tufOptions = append(tufOptions, tufConfig{
tufOptions: tufOpt,
targets: []string{defaultTR},
})
} else {
// Get from both Sigstore public good and GitHub private instance
tufOptions = append(tufOptions, tufOpt)
tufOptions = append(tufOptions, tufConfig{
tufOptions: tufOpt,
targets: []string{defaultTR},
})
tufOpt = verification.GitHubTUFOptions()
tufOpt.CacheValidity = 0
tufOptions = append(tufOptions, tufOpt)
targets := []string{defaultTR}
if opts.TrustDomain != "" {
targets = append(targets, fmt.Sprintf("%s.%s",
opts.TrustDomain, defaultTR))
}
tufOptions = append(tufOptions, tufConfig{
tufOptions: tufOpt,
targets: targets,
})
}
for _, tufOpt = range tufOptions {
tufClient, err := makeTUF(tufOpt)
for _, tufOpt := range tufOptions {
tufClient, err := makeTUF(tufOpt.tufOptions)
if err != nil {
return fmt.Errorf("failed to create TUF client: %v", err)
}
t, err := tufClient.GetTarget("trusted_root.json")
if err != nil {
return err
}
for _, target := range tufOpt.targets {
t, err := tufClient.GetTarget(target)
if err != nil {
return fmt.Errorf("failed to retrieve trusted root %s via TUF: %w",
target, err)
}
output := new(bytes.Buffer)
err = json.Compact(output, t)
if err != nil {
return err
}
output := new(bytes.Buffer)
err = json.Compact(output, t)
if err != nil {
return err
}
if !opts.VerifyOnly {
fmt.Println(output)
} else {
fmt.Printf("Local TUF repository for %s updated and verified\n", tufOpt.RepositoryBaseURL)
if !opts.VerifyOnly {
fmt.Println(output)
} else {
fmt.Printf("Local TUF repository for %s updated and verified\n", tufOpt.tufOptions.RepositoryBaseURL)
}
}
}

View file

@ -1,28 +1,50 @@
package verification
import (
"errors"
"fmt"
"strings"
)
func VerifyCertExtensions(results []*AttestationProcessingResult, owner string, repo string) error {
for _, attestation := range results {
// TODO: handle proxima prefix
expectedSourceRepositoryOwnerURI := fmt.Sprintf("https://github.com/%s", owner)
sourceRepositoryOwnerURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI
if !strings.EqualFold(expectedSourceRepositoryOwnerURI, sourceRepositoryOwnerURI) {
return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", expectedSourceRepositoryOwnerURI, sourceRepositoryOwnerURI)
}
func VerifyCertExtensions(results []*AttestationProcessingResult, tenant, owner, repo string) error {
if len(results) == 0 {
return errors.New("no attestations proccessing results")
}
// if repo is set, check the SourceRepositoryURI field
if repo != "" {
// TODO: handle proxima prefix
expectedSourceRepositoryURI := fmt.Sprintf("https://github.com/%s", repo)
sourceRepositoryURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryURI
if !strings.EqualFold(expectedSourceRepositoryURI, sourceRepositoryURI) {
return fmt.Errorf("expected SourceRepositoryURI to be %s, got %s", expectedSourceRepositoryURI, sourceRepositoryURI)
}
for _, attestation := range results {
if err := verifyCertExtensions(attestation, tenant, owner, repo); err != nil {
return err
}
}
return nil
}
func verifyCertExtensions(attestation *AttestationProcessingResult, tenant, owner, repo string) error {
var want string
if tenant == "" {
want = fmt.Sprintf("https://github.com/%s", owner)
} else {
want = fmt.Sprintf("https://%s.ghe.com/%s", tenant, owner)
}
sourceRepositoryOwnerURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI
if !strings.EqualFold(want, sourceRepositoryOwnerURI) {
return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", want, sourceRepositoryOwnerURI)
}
// if repo is set, check the SourceRepositoryURI field
if repo != "" {
if tenant == "" {
want = fmt.Sprintf("https://github.com/%s", repo)
} else {
want = fmt.Sprintf("https://%s.ghe.com/%s", tenant, repo)
}
sourceRepositoryURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryURI
if !strings.EqualFold(want, sourceRepositoryURI) {
return fmt.Errorf("expected SourceRepositoryURI to be %s, got %s", want, sourceRepositoryURI)
}
}
return nil
}

View file

@ -25,22 +25,74 @@ func TestVerifyCertExtensions(t *testing.T) {
}
t.Run("VerifyCertExtensions with owner and repo", func(t *testing.T) {
err := VerifyCertExtensions(results, "owner", "owner/repo")
err := VerifyCertExtensions(results, "", "owner", "owner/repo")
require.NoError(t, err)
})
t.Run("VerifyCertExtensions with owner and repo, but wrong tenant", func(t *testing.T) {
err := VerifyCertExtensions(results, "foo", "owner", "owner/repo")
require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://foo.ghe.com/owner, got https://github.com/owner")
})
t.Run("VerifyCertExtensions with owner", func(t *testing.T) {
err := VerifyCertExtensions(results, "owner", "")
err := VerifyCertExtensions(results, "", "owner", "")
require.NoError(t, err)
})
t.Run("VerifyCertExtensions with wrong owner", func(t *testing.T) {
err := VerifyCertExtensions(results, "wrong", "")
err := VerifyCertExtensions(results, "", "wrong", "")
require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://github.com/wrong, got https://github.com/owner")
})
t.Run("VerifyCertExtensions with wrong repo", func(t *testing.T) {
err := VerifyCertExtensions(results, "owner", "wrong")
err := VerifyCertExtensions(results, "", "owner", "wrong")
require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://github.com/wrong, got https://github.com/owner/repo")
})
}
func TestVerifyTenancyCertExtensions(t *testing.T) {
results := []*AttestationProcessingResult{
{
VerificationResult: &verify.VerificationResult{
Signature: &verify.SignatureVerificationResult{
Certificate: &certificate.Summary{
Extensions: certificate.Extensions{
SourceRepositoryOwnerURI: "https://foo.ghe.com/owner",
SourceRepositoryURI: "https://foo.ghe.com/owner/repo",
},
},
},
},
},
}
t.Run("VerifyCertExtensions with owner and repo", func(t *testing.T) {
err := VerifyCertExtensions(results, "foo", "owner", "owner/repo")
require.NoError(t, err)
})
t.Run("VerifyCertExtensions with owner and repo, no tenant", func(t *testing.T) {
err := VerifyCertExtensions(results, "", "owner", "owner/repo")
require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://github.com/owner, got https://foo.ghe.com/owner")
})
t.Run("VerifyCertExtensions with owner and repo, wrong tenant", func(t *testing.T) {
err := VerifyCertExtensions(results, "bar", "owner", "owner/repo")
require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://bar.ghe.com/owner, got https://foo.ghe.com/owner")
})
t.Run("VerifyCertExtensions with owner", func(t *testing.T) {
err := VerifyCertExtensions(results, "foo", "owner", "")
require.NoError(t, err)
})
t.Run("VerifyCertExtensions with wrong owner", func(t *testing.T) {
err := VerifyCertExtensions(results, "foo", "wrong", "")
require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://foo.ghe.com/wrong, got https://foo.ghe.com/owner")
})
t.Run("VerifyCertExtensions with wrong repo", func(t *testing.T) {
err := VerifyCertExtensions(results, "foo", "owner", "wrong")
require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://foo.ghe.com/wrong, got https://foo.ghe.com/owner/repo")
})
}

View file

@ -36,6 +36,8 @@ type SigstoreConfig struct {
TrustedRoot string
Logger *io.Handler
NoPublicGood bool
// If tenancy mode is not used, trust domain is empty
TrustDomain string
}
type SigstoreVerifier interface {
@ -144,7 +146,7 @@ func (v *LiveSigstoreVerifier) chooseVerifier(b *bundle.Bundle) (*verify.SignedE
return publicGoodVerifier, issuer, nil
} else if leafCert.Issuer.Organization[0] == GitHubIssuerOrg || v.config.NoPublicGood {
ghVerifier, err := newGitHubVerifier()
ghVerifier, err := newGitHubVerifier(v.config.TrustDomain)
if err != nil {
return nil, "", fmt.Errorf("failed to create GitHub Sigstore verifier: %v", err)
}
@ -240,13 +242,25 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerif
return gv, nil
}
func newGitHubVerifier() (*verify.SignedEntityVerifier, error) {
func newGitHubVerifier(trustDomain string) (*verify.SignedEntityVerifier, error) {
var tr string
opts := GitHubTUFOptions()
client, err := tuf.New(opts)
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %v", err)
}
trustedRoot, err := root.GetTrustedRoot(client)
if trustDomain == "" {
tr = "trusted_root.json"
} else {
tr = fmt.Sprintf("%s.trusted_root.json", trustDomain)
}
jsonBytes, err := client.GetTarget(tr)
if err != nil {
return nil, err
}
trustedRoot, err := root.NewTrustedRootFromJSON(jsonBytes)
if err != nil {
return nil, err
}

View file

@ -6,6 +6,7 @@ import (
"strings"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghinstance"
"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/io"
@ -37,6 +38,9 @@ type Options struct {
OCIClient oci.Client
SigstoreVerifier verification.SigstoreVerifier
exporter cmdutil.Exporter
Hostname string
// Tenant is only set when tenancy is used
Tenant string
}
// Clean cleans the file path option values
@ -57,12 +61,12 @@ func (opts *Options) SetPolicyFlags() {
opts.Owner = splitRepo[0]
if !isSignerIdentityProvided(opts) {
opts.SANRegex = expandToGitHubURL(opts.Repo)
opts.SANRegex = expandToGitHubURL(opts.Tenant, opts.Repo)
}
return
}
if !isSignerIdentityProvided(opts) {
opts.SANRegex = expandToGitHubURL(opts.Owner)
opts.SANRegex = expandToGitHubURL(opts.Tenant, opts.Owner)
}
}
@ -94,6 +98,13 @@ func (opts *Options) AreFlagsValid() error {
return fmt.Errorf("bundle-from-oci flag cannot be used with bundle-path flag")
}
// Verify provided hostname
if opts.Hostname != "" {
if err := ghinstance.HostnameValidator(opts.Hostname); err != nil {
return fmt.Errorf("error parsing hostname: %w", err)
}
}
return nil
}

View file

@ -1,8 +1,8 @@
package verify
import (
"errors"
"fmt"
"os"
"regexp"
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
@ -13,21 +13,23 @@ import (
)
const (
GitHubOIDCIssuer = "https://token.actions.githubusercontent.com"
GitHubOIDCIssuer = "https://token.actions.githubusercontent.com"
GitHubTenantOIDCIssuer = "https://token.actions.%s.ghe.com"
// represents the GitHub hosted runner in the certificate RunnerEnvironment extension
GitHubRunner = "github-hosted"
githubHost = "github.com"
hostRegex = `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*$`
)
func expandToGitHubURL(ownerOrRepo string) string {
// TODO: handle proxima prefix
return fmt.Sprintf("(?i)^https://github.com/%s/", ownerOrRepo)
func expandToGitHubURL(tenant, ownerOrRepo string) string {
if tenant == "" {
return fmt.Sprintf("(?i)^https://github.com/%s/", ownerOrRepo)
}
return fmt.Sprintf("(?i)^https://%s.ghe.com/%s/", tenant, ownerOrRepo)
}
func buildSANMatcher(opts *Options) (verify.SubjectAlternativeNameMatcher, error) {
if opts.SignerRepo != "" {
signedRepoRegex := expandToGitHubURL(opts.SignerRepo)
signedRepoRegex := expandToGitHubURL(opts.Tenant, opts.SignerRepo)
return verify.NewSANMatcher("", signedRepoRegex)
} else if opts.SignerWorkflow != "" {
validatedWorkflowRegex, err := validateSignerWorkflow(opts)
@ -118,37 +120,9 @@ func validateSignerWorkflow(opts *Options) (string, error) {
return addSchemeToRegex(opts.SignerWorkflow), nil
}
// if the provided workflow does not contain a host, check for a host
// and prepend it to the workflow
host, err := chooseHost(opts)
if err != nil {
return "", err
if opts.Hostname == "" {
return "", errors.New("unknown host")
}
return addSchemeToRegex(fmt.Sprintf("%s/%s", host, opts.SignerWorkflow)), nil
}
// if a host was not provided as part of a flag argument choose a host based
// on gh cli configuration
func chooseHost(opts *Options) (string, error) {
// check if GH_HOST is set and use that host if it is
host := os.Getenv("GH_HOST")
if host != "" {
return host, nil
}
// check if the CLI is authenticated with any hosts
cfg, err := opts.Config()
if err != nil {
return "", err
}
// if authenticated, return the authenticated host
authCfg := cfg.Authentication()
if host, _ := authCfg.DefaultHost(); host != "" {
return host, nil
}
// if not authenticated, return the default host github.com
return githubHost, nil
return addSchemeToRegex(fmt.Sprintf("%s/%s", opts.Hostname, opts.SignerWorkflow)), nil
}

View file

@ -1,7 +1,6 @@
package verify
import (
"os"
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
@ -32,13 +31,12 @@ func TestBuildPolicy(t *testing.T) {
require.NoError(t, err)
}
func ValidateSignerWorkflow(t *testing.T) {
func TestValidateSignerWorkflow(t *testing.T) {
type testcase struct {
name string
providedSignerWorkflow string
expectedWorkflowRegex string
ghHost string
authHost string
host string
}
testcases := []testcase{
@ -56,13 +54,19 @@ func ValidateSignerWorkflow(t *testing.T) {
name: "workflow with GH_HOST set",
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://myhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
ghHost: "myhost.github.com",
host: "myhost.github.com",
},
{
name: "workflow with authenticated host",
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
authHost: "authedhost.github.com",
host: "authedhost.github.com",
},
{
name: "workflow with authenticated host",
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
host: "authedhost.github.com",
},
}
@ -74,22 +78,29 @@ func ValidateSignerWorkflow(t *testing.T) {
SignerWorkflow: tc.providedSignerWorkflow,
}
if tc.ghHost != "" {
err := os.Setenv("GH_HOST", tc.ghHost)
require.NoError(t, err)
}
if tc.authHost != "" {
cfg, err := opts.Config()
require.NoError(t, err)
// if authenticated, return the authenticated host
authCfg := cfg.Authentication()
authCfg.SetDefaultHost(tc.authHost, "")
// All host resolution is done verify.go:RunE
if tc.host == "" {
// Set to default host
tc.host = "github.com"
}
opts.Hostname = tc.host
workflowRegex, err := validateSignerWorkflow(opts)
require.NoError(t, err)
require.Equal(t, tc.expectedWorkflowRegex, workflowRegex)
}
}
func TestValidateSignerWorkflowNoHost(t *testing.T) {
cmdFactory := factory.New("test")
opts := &Options{
Config: cmdFactory.Config,
SignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
}
workflowRegex, err := validateSignerWorkflow(opts)
require.Error(t, err)
require.ErrorContains(t, err, "unknown host")
require.Equal(t, "", workflowRegex)
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"regexp"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
@ -13,6 +14,7 @@ import (
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
ghauth "github.com/cli/go-gh/v2/pkg/auth"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
@ -109,9 +111,6 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
// Clean file path options
opts.Clean()
// set policy flags based on what has been provided
opts.SetPolicyFlags()
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
@ -119,17 +118,18 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
if err != nil {
return err
}
opts.APIClient = api.NewLiveClient(hc, opts.Logger)
opts.OCIClient = oci.NewLiveClient()
if err := auth.IsHostSupported(); err != nil {
if opts.Hostname == "" {
opts.Hostname, _ = ghauth.DefaultHost()
}
err = auth.IsHostSupported(opts.Hostname)
if err != nil {
return err
}
if runF != nil {
return runF(opts)
}
opts.APIClient = api.NewLiveClient(hc, opts.Hostname, opts.Logger)
config := verification.SigstoreConfig{
TrustedRoot: opts.TrustedRoot,
@ -137,6 +137,30 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
NoPublicGood: opts.NoPublicGood,
}
// Prepare for tenancy if detected
if ghinstance.IsTenancy(opts.Hostname) {
td, err := opts.APIClient.GetTrustDomain()
if err != nil {
return fmt.Errorf("error getting trust domain, make sure you are authenticated against the host: %w", err)
}
tenant, found := ghinstance.TenantName(opts.Hostname)
if !found {
return fmt.Errorf("invalid hostname provided: '%s'",
opts.Hostname)
}
config.TrustDomain = td
opts.Tenant = tenant
opts.OIDCIssuer = fmt.Sprintf(GitHubTenantOIDCIssuer, tenant)
}
// set policy flags based on what has been provided
opts.SetPolicyFlags()
if runF != nil {
return runF(opts)
}
opts.SigstoreVerifier = verification.NewLiveSigstoreVerifier(config)
opts.Config = f.Config
@ -169,6 +193,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]<owner>/<repo>/<path>/<to>/<workflow>")
verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex", "signer-repo", "signer-workflow")
verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", GitHubOIDCIssuer, "Issuer of the OIDC token")
verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")
return verifyCmd
}
@ -244,7 +269,7 @@ func runVerify(opts *Options) error {
}
// Verify extensions
if err := verification.VerifyCertExtensions(sigstoreRes.VerifyResults, opts.Owner, opts.Repo); err != nil {
if err := verification.VerifyCertExtensions(sigstoreRes.VerifyResults, opts.Tenant, opts.Owner, opts.Repo); err != nil {
opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Verification failed"))
return err
}
@ -264,7 +289,7 @@ func runVerify(opts *Options) error {
opts.Logger.Printf("%s was attested by:\n", artifact.DigestWithAlg())
// Otherwise print the results to the terminal in a table
tableContent, err := buildTableVerifyContent(sigstoreRes.VerifyResults)
tableContent, err := buildTableVerifyContent(opts.Tenant, sigstoreRes.VerifyResults)
if err != nil {
opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse results"))
return err
@ -280,30 +305,44 @@ func runVerify(opts *Options) error {
return nil
}
func extractAttestationDetail(builderSignerURI string) (string, string, error) {
func extractAttestationDetail(tenant, builderSignerURI string) (string, string, error) {
// If given a build signer URI like
// https://github.com/foo/bar/.github/workflows/release.yml@refs/heads/main
// We want to extract:
// * foo/bar
// * .github/workflows/release.yml@refs/heads/main
orgAndRepoRegexp := regexp.MustCompile(`https://github\.com/([^/]+/[^/]+)/`)
var orgAndRepoRegexp *regexp.Regexp
var workflowRegexp *regexp.Regexp
if tenant == "" {
orgAndRepoRegexp = regexp.MustCompile(`https://github\.com/([^/]+/[^/]+)/`)
workflowRegexp = regexp.MustCompile(`https://github\.com/[^/]+/[^/]+/(.+)`)
} else {
var tr = regexp.QuoteMeta(tenant)
orgAndRepoRegexp = regexp.MustCompile(fmt.Sprintf(
`https://%s\.ghe\.com/([^/]+/[^/]+)/`,
tr))
workflowRegexp = regexp.MustCompile(fmt.Sprintf(
`https://%s\.ghe\.com/[^/]+/[^/]+/(.+)`,
tr))
}
match := orgAndRepoRegexp.FindStringSubmatch(builderSignerURI)
if len(match) < 2 {
return "", "", fmt.Errorf("no match found for org and repo")
}
repoAndOrg := match[1]
orgAndRepo := match[1]
workflowRegexp := regexp.MustCompile(`https://github\.com/[^/]+/[^/]+/(.+)`)
match = workflowRegexp.FindStringSubmatch(builderSignerURI)
if len(match) < 2 {
return "", "", fmt.Errorf("no match found for workflow")
}
workflow := match[1]
return repoAndOrg, workflow, nil
return orgAndRepo, workflow, nil
}
func buildTableVerifyContent(results []*verification.AttestationProcessingResult) ([][]string, error) {
func buildTableVerifyContent(tenant string, results []*verification.AttestationProcessingResult) ([][]string, error) {
content := make([][]string, len(results))
for i, res := range results {
@ -313,7 +352,7 @@ func buildTableVerifyContent(results []*verification.AttestationProcessingResult
return nil, fmt.Errorf("bundle missing verification result fields")
}
builderSignerURI := res.VerificationResult.Signature.Certificate.Extensions.BuildSignerURI
repoAndOrg, workflow, err := extractAttestationDetail(builderSignerURI)
repoAndOrg, workflow, err := extractAttestationDetail(tenant, builderSignerURI)
if err != nil {
return nil, err
}

View file

@ -11,6 +11,7 @@ import (
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmd/factory"
"github.com/cli/go-gh/v2/pkg/auth"
"github.com/stretchr/testify/require"
)
@ -28,8 +29,10 @@ func TestVerifyIntegration(t *testing.T) {
t.Fatal(err)
}
host, _ := auth.DefaultHost()
publicGoodOpts := Options{
APIClient: api.NewLiveClient(hc, logger),
APIClient: api.NewLiveClient(hc, host, logger),
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha512",
@ -99,8 +102,10 @@ func TestVerifyIntegrationReusableWorkflow(t *testing.T) {
t.Fatal(err)
}
host, _ := auth.DefaultHost()
baseOpts := Options{
APIClient: api.NewLiveClient(hc, logger),
APIClient: api.NewLiveClient(hc, host, logger),
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha256",
@ -185,8 +190,10 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) {
t.Fatal(err)
}
host, _ := auth.DefaultHost()
baseOpts := Options{
APIClient: api.NewLiveClient(hc, logger),
APIClient: api.NewLiveClient(hc, host, logger),
ArtifactPath: artifactPath,
BundlePath: bundlePath,
Config: cmdFactory.Config,
@ -203,6 +210,7 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) {
name string
signerWorkflow string
expectErr bool
host string
}
testcases := []testcase{
@ -220,12 +228,14 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) {
name: "valid signer workflow without host (defaults to github.com)",
signerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectErr: false,
host: "github.com",
},
}
for _, tc := range testcases {
opts := baseOpts
opts.SignerWorkflow = tc.signerWorkflow
opts.Hostname = tc.host
err := runVerify(&opts)
if tc.expectErr {

View file

@ -35,10 +35,21 @@ var (
func TestNewVerifyCmd(t *testing.T) {
testIO, _, _, _ := iostreams.Test()
var testReg httpmock.Registry
var metaResp = api.MetaResponse{
Domains: api.Domain{
ArtifactAttestations: api.ArtifactAttestations{
TrustDomain: "foo",
},
},
}
testReg.Register(httpmock.REST(http.MethodGet, "api/v3/meta"),
httpmock.StatusJSONResponse(200, &metaResp))
f := &cmdutil.Factory{
IOStreams: testIO,
HttpClient: func() (*http.Client, error) {
reg := &httpmock.Registry{}
reg := &testReg
client := &http.Client{}
httpmock.ReplaceTripper(client, reg)
return client, nil
@ -63,6 +74,7 @@ func TestNewVerifyCmd(t *testing.T) {
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsErr: true,
},
@ -78,9 +90,42 @@ func TestNewVerifyCmd(t *testing.T) {
Owner: "sigstore",
SANRegex: "(?i)^https://github.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsErr: false,
},
{
name: "Custom host",
cli: fmt.Sprintf("%s --bundle %s --owner sigstore --hostname foo.ghe.com", artifactPath, bundlePath),
wants: 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: "sha256",
Limit: 30,
OIDCIssuer: "https://token.actions.foo.ghe.com",
Owner: "sigstore",
SANRegex: "(?i)^https://foo.ghe.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "foo.ghe.com",
},
wantsErr: false,
},
{
name: "Invalid custom host",
cli: fmt.Sprintf("%s --bundle %s --owner sigstore --hostname foo.bar.com", artifactPath, bundlePath),
wants: 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: "sha256",
Limit: 30,
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SANRegex: "(?i)^https://github.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "foo.ghe.com",
},
wantsErr: true,
},
{
name: "Use custom digest-alg value",
cli: fmt.Sprintf("%s --bundle %s --owner sigstore --digest-alg sha512", artifactPath, bundlePath),
@ -93,6 +138,7 @@ func TestNewVerifyCmd(t *testing.T) {
Owner: "sigstore",
SANRegex: "(?i)^https://github.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsErr: false,
},
@ -107,6 +153,7 @@ func TestNewVerifyCmd(t *testing.T) {
Limit: 30,
SANRegex: "(?i)^https://github.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsErr: true,
},
@ -121,6 +168,7 @@ func TestNewVerifyCmd(t *testing.T) {
Repo: "sigstore/sigstore-js",
Limit: 30,
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsErr: true,
},
@ -135,6 +183,7 @@ func TestNewVerifyCmd(t *testing.T) {
Owner: "sigstore",
SANRegex: "(?i)^https://github.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsErr: false,
},
@ -149,6 +198,7 @@ func TestNewVerifyCmd(t *testing.T) {
Limit: 101,
SANRegex: "(?i)^https://github.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsErr: false,
},
@ -163,6 +213,7 @@ func TestNewVerifyCmd(t *testing.T) {
Limit: 0,
SANRegex: "(?i)^https://github.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsErr: true,
},
@ -178,6 +229,7 @@ func TestNewVerifyCmd(t *testing.T) {
SAN: "https://github.com/sigstore/",
SANRegex: "(?i)^https://github.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsErr: true,
},
@ -193,6 +245,7 @@ func TestNewVerifyCmd(t *testing.T) {
Owner: "sigstore",
SANRegex: "(?i)^https://github.com/sigstore/",
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
Hostname: "github.com",
},
wantsExporter: true,
},
@ -230,6 +283,7 @@ func TestNewVerifyCmd(t *testing.T) {
assert.Equal(t, tc.wants.Repo, opts.Repo)
assert.Equal(t, tc.wants.SAN, opts.SAN)
assert.Equal(t, tc.wants.SANRegex, opts.SANRegex)
assert.Equal(t, tc.wants.Hostname, opts.Hostname)
assert.NotNil(t, opts.APIClient)
assert.NotNil(t, opts.Logger)
assert.NotNil(t, opts.OCIClient)
@ -357,6 +411,17 @@ func TestRunVerify(t *testing.T) {
require.Nil(t, runVerify(&opts))
})
// Test with bad tenancy
t.Run("with bad tenancy", func(t *testing.T) {
opts := publicGoodOpts
opts.BundlePath = ""
opts.Repo = "sigstore/sigstore-js"
opts.Tenant = "foo"
err := runVerify(&opts)
require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://foo.ghe.com/sigstore, got https://github.com/sigstore")
})
t.Run("with repo which not matches SourceRepositoryURI", func(t *testing.T) {
opts := publicGoodOpts
opts.BundlePath = ""