diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index f96ca4e8a..34a56294b 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -28,17 +28,28 @@ func buildSANMatcher(san, sanRegex string) (verify.SubjectAlternativeNameMatcher return sanMatcher, nil } +func buildCertExtensions(opts *Options, runnerEnv string) certificate.Extensions { + extensions := certificate.Extensions{ + Issuer: opts.OIDCIssuer, + SourceRepositoryOwnerURI: fmt.Sprintf("https://github.com/%s", opts.Owner), + RunnerEnvironment: runnerEnv, + } + + // if opts.Repo is set, set the SourceRepositoryURI field before returning the extensions + if opts.Repo != "" { + extensions.SourceRepositoryURI = fmt.Sprintf("https://github.com/%s", opts.Repo) + } + return extensions +} + func buildCertificateIdentityOption(opts *Options, runnerEnv string) (verify.PolicyOption, error) { sanMatcher, err := buildSANMatcher(opts.SAN, opts.SANRegex) if err != nil { return nil, err } - extensions := certificate.Extensions{ - Issuer: opts.OIDCIssuer, - SourceRepositoryOwnerURI: fmt.Sprintf("https://github.com/%s", opts.Owner), - RunnerEnvironment: runnerEnv, - } + extensions := buildCertExtensions(opts, runnerEnv) + certId, err := verify.NewCertificateIdentity(sanMatcher, extensions) if err != nil { return nil, err diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 08322771d..182a85722 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -54,6 +54,16 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command To see the full results that are generated upon successful verification, i.e. for use with a policy engine, provide the %[1]s--json-result%[1]s flag. + The attestation's certificate's Subject Alternative Name (SAN) identifies the entity + responsible for creating the attestation, which most of the time will be a GitHub + Actions workflow file located inside your repository. By default, this command uses + either the %[1]s--repo%[1]s or the %[1]s--owner%[1]s flag value to validate the SAN. + + However, if you generate attestations with a reusable workflow then the SAN will + identify the reusable workflow – which may or may not be located inside your %[1]s--repo%[1]s + or %[1]s--owner%[1]s. In these situations, you can use the %[1]s--cert-identity%[1]s or + %[1]s--cert-identity-regex%[1]s flags to specify the reusable workflow's URI. + For more policy verification options, see the other available flags. `, "`"), Example: heredoc.Doc(` diff --git a/pkg/cmd/attestation/verify/verify_integration_test.go b/pkg/cmd/attestation/verify/verify_integration_test.go new file mode 100644 index 000000000..7cc1c8110 --- /dev/null +++ b/pkg/cmd/attestation/verify/verify_integration_test.go @@ -0,0 +1,82 @@ +//go:build integration + +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/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + "github.com/cli/cli/v2/pkg/cmd/factory" + "github.com/stretchr/testify/require" +) + +func TestVerifyIntegration(t *testing.T) { + logger := io.NewTestHandler() + + sigstoreConfig := verification.SigstoreConfig{ + Logger: logger, + } + + cmdFactory := factory.New("test") + + hc, err := cmdFactory.HttpClient() + if err != nil { + t.Fatal(err) + } + + publicGoodOpts := Options{ + APIClient: api.NewLiveClient(hc, logger), + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha512", + Logger: logger, + OCIClient: oci.NewLiveClient(), + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), + } + + t.Run("with valid owner", func(t *testing.T) { + err := runVerify(&publicGoodOpts) + require.NoError(t, err) + }) + + t.Run("with valid repo", func(t *testing.T) { + opts := publicGoodOpts + opts.Repo = "sigstore/sigstore-js" + + err := runVerify(&opts) + require.NoError(t, err) + }) + + t.Run("with valid owner and invalid repo", func(t *testing.T) { + opts := publicGoodOpts + opts.Repo = "sigstore/fakerepo" + + err := runVerify(&opts) + require.Error(t, err) + require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\": failed to verify certificate identity: no matching certificate identity found") + }) + + t.Run("with invalid owner", func(t *testing.T) { + opts := publicGoodOpts + opts.Owner = "fakeowner" + + err := runVerify(&opts) + require.Error(t, err) + require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\": failed to verify certificate identity: no matching certificate identity found") + }) + + t.Run("with invalid owner and invalid repo", func(t *testing.T) { + opts := publicGoodOpts + opts.Repo = "fakeowner/fakerepo" + + err := runVerify(&opts) + require.Error(t, err) + require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\": failed to verify certificate identity: no matching certificate identity found") + }) +}