add signer and source ref, commit options

Signed-off-by: Meredith Lancaster <malancas@github.com>
This commit is contained in:
Meredith Lancaster 2025-01-30 07:43:13 -07:00
parent 728aa3d83f
commit 313faf9cd0
3 changed files with 81 additions and 30 deletions

View file

@ -13,8 +13,7 @@ import (
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
)
const hostRegex = `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*$`
const workflowURIRegex = `^https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*\/.github\/workflows\/[a-zA-Z0-9-]+.(yml|yaml)$`
const workflowURIRegex = `^https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*\/.github\/workflows\/.*\/[a-zA-Z0-9-]+.(yml|yaml)$`
func expandToGitHubURL(tenant, ownerOrRepo string) string {
if tenant == "" {
@ -57,11 +56,17 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er
signedRepoRegex := expandToGitHubURLRegex(opts.Tenant, opts.SignerRepo)
c.SANRegex = signedRepoRegex
} else if opts.SignerWorkflow != "" {
validatedWorkflowRegex, err := validateSignerWorkflow(opts.Hostname, opts.SignerWorkflow)
validatedWorkflow, err := validateSignerWorkflow(opts.Hostname, opts.SignerWorkflow)
if err != nil {
return verification.EnforcementCriteria{}, err
}
c.SANRegex = validatedWorkflowRegex
workflowRegex := fmt.Sprintf("^%s", validatedWorkflow)
c.SANRegex = workflowRegex
if opts.SignerRef != "" {
c.Certificate.BuildSignerURI = fmt.Sprintf("%s@%s", validatedWorkflow, opts.SignerRef)
}
} else if opts.Repo != "" {
// if the user has not provided the SAN, SANRegex, SignerRepo, or SignerWorkflow options
// then we default to the repo option
@ -99,26 +104,11 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er
c.Certificate.Issuer = opts.OIDCIssuer
}
if opts.SignerDigest != "" {
c.Certificate.BuildSignerDigest = opts.SignerDigest
}
if opts.SignerRef != "" {
// need to build the full URI value
uri, err := getFullWorkflowURI(c.SANRegex)
if err != nil {
return verification.EnforcementCriteria{}, err
}
c.Certificate.BuildSignerURI = uri
}
if opts.SourceDigest != "" {
c.Certificate.SourceRepositoryDigest = opts.SourceDigest
}
if opts.SourceRef != "" {
c.Certificate.SourceRepositoryRef = opts.SourceRef
}
// set the SourceRepositoryDigest, SourceRepositoryRef, and BuildSignerDigest
// extensions if the options are provided
c.Certificate.BuildSignerDigest = opts.SignerDigest
c.Certificate.SourceRepositoryDigest = opts.SourceDigest
c.Certificate.SourceRepositoryRef = opts.SourceRef
return c, nil
}
@ -179,13 +169,13 @@ func buildSigstoreVerifyPolicy(c verification.EnforcementCriteria, a artifact.Di
func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) {
// we expect a provided workflow argument be in the format [HOST/]/<OWNER>/<REPO>/path/to/workflow.yml
// if the provided workflow does not contain a host, set the host
match, err := regexp.MatchString(hostRegex, signerWorkflow)
match, err := regexp.MatchString(workflowURIRegex, signerWorkflow)
if err != nil {
return "", err
}
if match {
return fmt.Sprintf("^https://%s", signerWorkflow), nil
return fmt.Sprintf("https://%s", signerWorkflow), nil
}
// if the provided workflow did not match the expect format
@ -194,5 +184,5 @@ func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) {
return "", errors.New("unknown host")
}
return fmt.Sprintf("^https://%s/%s", hostname, signerWorkflow), nil
return fmt.Sprintf("https://%s/%s", hostname, signerWorkflow), nil
}

View file

@ -217,6 +217,63 @@ func TestNewEnforcementCriteria(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "https://foo.com", c.Certificate.Issuer)
})
t.Run("sets Certificate.BuildSignerURI using SignerWorkflow and SignerRef", func(t *testing.T) {
opts := &Options{
ArtifactPath: artifactPath,
Owner: "wrong",
Repo: "wrong/value",
SignerWorkflow: "foo/bar/.github/workflows/attest.yml",
SignerRef: "refs/heads/main",
Hostname: "github.com",
}
c, err := newEnforcementCriteria(opts)
require.NoError(t, err)
require.Equal(t, "https://github.com/foo/bar/.github/workflows/attest.yml@refs/heads/main", c.Certificate.BuildSignerURI)
})
t.Run("sets Certificate.BuildSignerDigest using opts.SignerDigest", func(t *testing.T) {
opts := &Options{
ArtifactPath: artifactPath,
Owner: "wrong",
Repo: "wrong/value",
SignerDigest: "foo",
Hostname: "github.com",
}
c, err := newEnforcementCriteria(opts)
require.NoError(t, err)
require.Equal(t, "foo", c.Certificate.BuildSignerDigest)
})
t.Run("sets Certificate.SourceRepositoryDigest using opts.SourceDigest", func(t *testing.T) {
opts := &Options{
ArtifactPath: artifactPath,
Owner: "wrong",
Repo: "wrong/value",
SourceDigest: "foo",
Hostname: "github.com",
}
c, err := newEnforcementCriteria(opts)
require.NoError(t, err)
require.Equal(t, "foo", c.Certificate.SourceRepositoryDigest)
})
t.Run("sets Certificate.SourceRepositoryRef using opts.SourceRef", func(t *testing.T) {
opts := &Options{
ArtifactPath: artifactPath,
Owner: "wrong",
Repo: "wrong/value",
SourceRef: "refs/heads/main",
Hostname: "github.com",
}
c, err := newEnforcementCriteria(opts)
require.NoError(t, err)
require.Equal(t, "refs/heads/main", c.Certificate.SourceRepositoryRef)
})
}
func TestValidateSignerWorkflow(t *testing.T) {

View file

@ -187,14 +187,18 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch")
cmdutil.AddFormatFlags(verifyCmd, &opts.exporter)
// 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.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format <owner>/<repo>")
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", "", verification.GitHubOIDCIssuer, "Issuer of the OIDC token")
verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners")
verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")
verifyCmd.Flags().StringVarP(&opts.SignerDigest, "signer-digest", "", "", "Digest associated with the signer workflow")
verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format <owner>/<repo>")
verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]<owner>/<repo>/<path>/<to>/<workflow>")
verifyCmd.Flags().StringVarP(&opts.SignerRef, "signer-ref", "", "", "Ref associated with the signer workflow. signer-workflow must be provided to use this")
verifyCmd.Flags().StringVarP(&opts.SourceRef, "source-ref", "", "", "Ref associated with the source workflow")
verifyCmd.Flags().StringVarP(&opts.SourceDigest, "source-digest", "", "", "Digest associated with the source workflow")
return verifyCmd
}