From 2f81a33e95e3a0e1a44b017ecf6446cfcc5701b6 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Fri, 24 Jan 2025 09:28:55 -0700 Subject: [PATCH 01/10] add new signing options Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/options.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index 4296cb8ec..3ac642904 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -31,8 +31,12 @@ type Options struct { Repo string SAN string SANRegex string + SignerDigest string + SignerRef string SignerRepo string SignerWorkflow string + SourceRef string + SourceDigest string APIClient api.Client Logger *io.Handler OCIClient oci.Client From 11dc8d48f54e068ee8775f16faaaaf34c4fb515c Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Fri, 24 Jan 2025 13:19:47 -0700 Subject: [PATCH 02/10] reorder fields Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index 3ac642904..b53890431 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -35,8 +35,8 @@ type Options struct { SignerRef string SignerRepo string SignerWorkflow string - SourceRef string SourceDigest string + SourceRef string APIClient api.Client Logger *io.Handler OCIClient oci.Client From 728aa3d83fbc30d7978af82f31652cf9f81ae300 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Fri, 24 Jan 2025 13:20:01 -0700 Subject: [PATCH 03/10] set new options in enforcement criteria Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/policy.go | 38 ++++++++++++++++++++++- pkg/cmd/attestation/verify/policy_test.go | 14 +++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 1e1dcd4d8..bb6ddb278 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -14,6 +14,7 @@ import ( ) 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)$` func expandToGitHubURL(tenant, ownerOrRepo string) string { if tenant == "" { @@ -66,7 +67,7 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er // then we default to the repo option c.SANRegex = expandToGitHubURLRegex(opts.Tenant, opts.Repo) } else { - // if opts.Repo was not provided, we fallback to the opts.Owner value + // if opts.Repo was not provided, we fall back to the opts.Owner value c.SANRegex = expandToGitHubURLRegex(opts.Tenant, owner) } @@ -98,9 +99,44 @@ 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 + } + return c, nil } +func getFullWorkflowURI(s string) (string, error) { + trimmed, _ := strings.CutPrefix(s, "^") + match, err := regexp.MatchString(workflowURIRegex, trimmed) + if err != nil { + return "", err + } + + if !match { + return "", nil + } + + return trimmed, nil +} + func buildCertificateIdentityOption(c verification.EnforcementCriteria) (verify.PolicyOption, error) { sanMatcher, err := verify.NewSANMatcher(c.SAN, c.SANRegex) if err != nil { diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index d033ba4fa..ecaf74ec3 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -1,6 +1,7 @@ package verify import ( + "fmt" "testing" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" @@ -275,3 +276,16 @@ func TestValidateSignerWorkflow(t *testing.T) { } } } + +func TestGetFullWorkflowURI(t *testing.T) { + expectedURI := "https://github.com/foo/bar/.github/workflows/mybuildjob.yaml" + // exact matching + uri, err := getFullWorkflowURI(expectedURI) + require.NoError(t, err) + require.Equal(t, expectedURI, uri) + + // matching after stripping regex prefix characters + uri, err = getFullWorkflowURI(fmt.Sprintf("^%s", expectedURI)) + require.NoError(t, err) + require.Equal(t, expectedURI, uri) +} From 313faf9cd08d8aa47cb2b8118410852393b7597e Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 07:43:13 -0700 Subject: [PATCH 04/10] add signer and source ref, commit options Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/policy.go | 44 +++++++---------- pkg/cmd/attestation/verify/policy_test.go | 57 +++++++++++++++++++++++ pkg/cmd/attestation/verify/verify.go | 10 ++-- 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index bb6ddb278..759867509 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -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/]///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 } diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index ecaf74ec3..a05e3377d 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -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) { diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index ea7502f00..c1ec63bbb 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -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 /") - verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") 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 /") + verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") + 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 } From c6b5928ddc6b6e92524d4890192d417daf07d5d9 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 07:58:42 -0700 Subject: [PATCH 05/10] fix issues causing tests to fail Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/policy.go | 15 ++++++++++++++- pkg/cmd/attestation/verify/policy.go | 18 ++---------------- pkg/cmd/attestation/verify/policy_test.go | 22 ++++------------------ pkg/cmd/attestation/verify/verify.go | 2 +- 4 files changed, 21 insertions(+), 36 deletions(-) diff --git a/pkg/cmd/attestation/verification/policy.go b/pkg/cmd/attestation/verification/policy.go index f2bd126d0..487542a58 100644 --- a/pkg/cmd/attestation/verification/policy.go +++ b/pkg/cmd/attestation/verification/policy.go @@ -52,7 +52,7 @@ func (c EnforcementCriteria) Valid() error { } func (c EnforcementCriteria) BuildPolicyInformation() string { - policyAttr := make([][]string, 0, 6) + policyAttr := [][]string{} policyAttr = appendStr(policyAttr, "- Predicate type must match", c.PredicateType) @@ -62,6 +62,19 @@ func (c EnforcementCriteria) BuildPolicyInformation() string { policyAttr = appendStr(policyAttr, "- Source Repository URI must match", c.Certificate.SourceRepositoryURI) } + if c.Certificate.BuildSignerDigest != "" { + policyAttr = appendStr(policyAttr, "- Build signer digest must match", c.Certificate.BuildSignerDigest) + } + if c.Certificate.BuildSignerURI != "" { + policyAttr = appendStr(policyAttr, "- Build signer URI must match", c.Certificate.BuildSignerURI) + } + if c.Certificate.SourceRepositoryDigest != "" { + policyAttr = appendStr(policyAttr, "- Source repo digest digest must match", c.Certificate.SourceRepositoryDigest) + } + if c.Certificate.SourceRepositoryRef != "" { + policyAttr = appendStr(policyAttr, "- Source repo ref must match", c.Certificate.SourceRepositoryRef) + } + if c.SAN != "" { policyAttr = appendStr(policyAttr, "- Subject Alternative Name must match", c.SAN) } else if c.SANRegex != "" { diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 759867509..c42d873d6 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -13,7 +13,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/verification" ) -const workflowURIRegex = `^https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*\/.github\/workflows\/.*\/[a-zA-Z0-9-]+.(yml|yaml)$` +const hostRegex = `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*$` func expandToGitHubURL(tenant, ownerOrRepo string) string { if tenant == "" { @@ -113,20 +113,6 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er return c, nil } -func getFullWorkflowURI(s string) (string, error) { - trimmed, _ := strings.CutPrefix(s, "^") - match, err := regexp.MatchString(workflowURIRegex, trimmed) - if err != nil { - return "", err - } - - if !match { - return "", nil - } - - return trimmed, nil -} - func buildCertificateIdentityOption(c verification.EnforcementCriteria) (verify.PolicyOption, error) { sanMatcher, err := verify.NewSANMatcher(c.SAN, c.SANRegex) if err != nil { @@ -169,7 +155,7 @@ 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/]///path/to/workflow.yml // if the provided workflow does not contain a host, set the host - match, err := regexp.MatchString(workflowURIRegex, signerWorkflow) + match, err := regexp.MatchString(hostRegex, signerWorkflow) if err != nil { return "", err } diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index a05e3377d..83ab893de 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -1,7 +1,6 @@ package verify import ( - "fmt" "testing" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" @@ -296,25 +295,25 @@ func TestValidateSignerWorkflow(t *testing.T) { { name: "workflow with default host", providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", - expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", + expectedWorkflowRegex: "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "github.com", }, { name: "workflow with workflow URL included", providedSignerWorkflow: "github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", - expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", + expectedWorkflowRegex: "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "github.com", }, { 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", + expectedWorkflowRegex: "https://myhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", 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", + expectedWorkflowRegex: "https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "authedhost.github.com", }, } @@ -333,16 +332,3 @@ func TestValidateSignerWorkflow(t *testing.T) { } } } - -func TestGetFullWorkflowURI(t *testing.T) { - expectedURI := "https://github.com/foo/bar/.github/workflows/mybuildjob.yaml" - // exact matching - uri, err := getFullWorkflowURI(expectedURI) - require.NoError(t, err) - require.Equal(t, expectedURI, uri) - - // matching after stripping regex prefix characters - uri, err = getFullWorkflowURI(fmt.Sprintf("^%s", expectedURI)) - require.NoError(t, err) - require.Equal(t, expectedURI, uri) -} diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index c1ec63bbb..97fe85845 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -194,9 +194,9 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command 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.SignerRef, "signer-ref", "", "", "Ref associated with the signer workflow. signer-workflow must be provided to use this") verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format /") verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") - 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") From e10010c4cfc9de2c3f36c87a4cbf8726cd01c766 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 08:03:36 -0700 Subject: [PATCH 06/10] fix option ordering Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/verify.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 97fe85845..f95867e8a 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -187,16 +187,16 @@ 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 /") + verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") 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.SignerRef, "signer-ref", "", "", "Ref associated with the signer workflow. signer-workflow must be provided to use this") - verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format /") - verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") 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") From 1c326c74f01c0a7b817b79739b3a0479a6a3bed4 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 30 Jan 2025 08:14:13 -0700 Subject: [PATCH 07/10] add checks to cert extensions func Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/extensions.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index 2958408d0..6770dca63 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -59,5 +59,18 @@ func verifyCertExtensions(given, expected certificate.Summary) error { return fmt.Errorf("expected Issuer to be %s, got %s", expected.Issuer, given.Issuer) } + if expected.BuildSignerDigest != "" && !strings.EqualFold(expected.BuildSignerDigest, given.BuildSignerDigest) { + return fmt.Errorf("expected BuildSignerDigest to be %s, got %s", expected.BuildSignerDigest, given.BuildSignerDigest) + } + if expected.BuildSignerURI != "" && !strings.EqualFold(expected.BuildSignerURI, given.BuildSignerURI) { + return fmt.Errorf("expected BuildSignerURI to be %s, got %s", expected.BuildSignerURI, given.BuildSignerURI) + } + if expected.SourceRepositoryDigest != "" && !strings.EqualFold(expected.SourceRepositoryDigest, given.SourceRepositoryDigest) { + return fmt.Errorf("expected SourceRepositoryDigest to be %s, got %s", expected.SourceRepositoryDigest, given.SourceRepositoryDigest) + } + if expected.SourceRepositoryRef != "" && !strings.EqualFold(expected.SourceRepositoryRef, given.SourceRepositoryRef) { + return fmt.Errorf("expected SourceRepositoryRef to be %s, got %s", expected.SourceRepositoryRef, given.SourceRepositoryRef) + } + return nil } From ce87c746b2a1f84c6e4734a08705c02e998d359a Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 24 Feb 2025 10:44:29 -0700 Subject: [PATCH 08/10] remove signer-ref option Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/extensions.go | 3 --- pkg/cmd/attestation/verification/policy.go | 3 --- pkg/cmd/attestation/verify/options.go | 1 - pkg/cmd/attestation/verify/policy.go | 4 ---- pkg/cmd/attestation/verify/policy_test.go | 15 --------------- pkg/cmd/attestation/verify/verify.go | 1 - 6 files changed, 27 deletions(-) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index 6770dca63..3ac9ac0a0 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -62,9 +62,6 @@ func verifyCertExtensions(given, expected certificate.Summary) error { if expected.BuildSignerDigest != "" && !strings.EqualFold(expected.BuildSignerDigest, given.BuildSignerDigest) { return fmt.Errorf("expected BuildSignerDigest to be %s, got %s", expected.BuildSignerDigest, given.BuildSignerDigest) } - if expected.BuildSignerURI != "" && !strings.EqualFold(expected.BuildSignerURI, given.BuildSignerURI) { - return fmt.Errorf("expected BuildSignerURI to be %s, got %s", expected.BuildSignerURI, given.BuildSignerURI) - } if expected.SourceRepositoryDigest != "" && !strings.EqualFold(expected.SourceRepositoryDigest, given.SourceRepositoryDigest) { return fmt.Errorf("expected SourceRepositoryDigest to be %s, got %s", expected.SourceRepositoryDigest, given.SourceRepositoryDigest) } diff --git a/pkg/cmd/attestation/verification/policy.go b/pkg/cmd/attestation/verification/policy.go index 487542a58..284560466 100644 --- a/pkg/cmd/attestation/verification/policy.go +++ b/pkg/cmd/attestation/verification/policy.go @@ -65,9 +65,6 @@ func (c EnforcementCriteria) BuildPolicyInformation() string { if c.Certificate.BuildSignerDigest != "" { policyAttr = appendStr(policyAttr, "- Build signer digest must match", c.Certificate.BuildSignerDigest) } - if c.Certificate.BuildSignerURI != "" { - policyAttr = appendStr(policyAttr, "- Build signer URI must match", c.Certificate.BuildSignerURI) - } if c.Certificate.SourceRepositoryDigest != "" { policyAttr = appendStr(policyAttr, "- Source repo digest digest must match", c.Certificate.SourceRepositoryDigest) } diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index b53890431..0fbbec55a 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -32,7 +32,6 @@ type Options struct { SAN string SANRegex string SignerDigest string - SignerRef string SignerRepo string SignerWorkflow string SourceDigest string diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index c42d873d6..1434200d5 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -63,10 +63,6 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er 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 diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index 83ab893de..04413fb78 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -217,21 +217,6 @@ func TestNewEnforcementCriteria(t *testing.T) { 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, diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index f95867e8a..f777ef0e8 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -196,7 +196,6 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verification.GitHubOIDCIssuer, "Issuer of the OIDC token") 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.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") From 37a91ebfdb2790ed7d1c96428198355f1ce16e35 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 24 Feb 2025 10:47:39 -0700 Subject: [PATCH 09/10] undo regex changes Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/policy.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 1434200d5..1060a781e 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -56,13 +56,11 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er signedRepoRegex := expandToGitHubURLRegex(opts.Tenant, opts.SignerRepo) c.SANRegex = signedRepoRegex } else if opts.SignerWorkflow != "" { - validatedWorkflow, err := validateSignerWorkflow(opts.Hostname, opts.SignerWorkflow) + validatedWorkflowRegex, err := validateSignerWorkflow(opts.Hostname, opts.SignerWorkflow) if err != nil { return verification.EnforcementCriteria{}, err } - - workflowRegex := fmt.Sprintf("^%s", validatedWorkflow) - c.SANRegex = workflowRegex + c.SANRegex = validatedWorkflowRegex } else if opts.Repo != "" { // if the user has not provided the SAN, SANRegex, SignerRepo, or SignerWorkflow options // then we default to the repo option @@ -157,7 +155,7 @@ func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) { } 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 @@ -166,5 +164,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 } From 343d9babeb6b08eb9503c11ad7543429c33fb6f7 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 24 Feb 2025 10:51:34 -0700 Subject: [PATCH 10/10] fix expected test output Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/policy_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index 04413fb78..d376498b6 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -280,25 +280,25 @@ func TestValidateSignerWorkflow(t *testing.T) { { name: "workflow with default host", providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", - expectedWorkflowRegex: "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", + expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "github.com", }, { name: "workflow with workflow URL included", providedSignerWorkflow: "github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", - expectedWorkflowRegex: "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", + expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "github.com", }, { 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", + expectedWorkflowRegex: "^https://myhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", 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", + expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", host: "authedhost.github.com", }, }