diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index abebba9e3..b4672213d 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "regexp" + "strings" "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" "github.com/sigstore/sigstore-go/pkg/verify" @@ -20,11 +21,13 @@ const ( ) type ExpectedExtensions struct { - RunnerEnvironment string - SANRegex string - SAN string - BuildSourceRepo string - SignerWorkflow string + RunnerEnvironment string + SANRegex string + SAN string + BuildSourceRepoURI string + SignerWorkflow string + SourceRepositoryOwnerURI string + SourceRepositoryURI string } type SigstoreInstance string @@ -40,10 +43,13 @@ type Policy struct { ExpectedPredicateType string ExpectedSigstoreInstance string Artifact artifact.DigestedArtifact + OIDCIssuer string } func newPolicy(opts *Options, a artifact.DigestedArtifact) (Policy, error) { - p := Policy{} + p := Policy{ + Artifact: a, + } if opts.SignerRepo != "" { signedRepoRegex := expandToGitHubURL(opts.Tenant, opts.SignerRepo) @@ -68,10 +74,25 @@ func newPolicy(opts *Options, a artifact.DigestedArtifact) (Policy, error) { if opts.Repo != "" { if opts.Tenant != "" { - p.ExpectedExtensions.BuildSourceRepo = fmt.Sprintf("https://%s.ghe.com/%s", opts.Tenant, opts.Repo) + p.ExpectedExtensions.BuildSourceRepoURI = fmt.Sprintf("https://%s.ghe.com/%s", opts.Tenant, opts.Repo) } - p.ExpectedExtensions.BuildSourceRepo = fmt.Sprintf("https://github.com/%s", opts.Repo) + p.ExpectedExtensions.BuildSourceRepoURI = fmt.Sprintf("https://github.com/%s", opts.Repo) } + + if opts.Tenant != "" { + p.ExpectedExtensions.SourceRepositoryOwnerURI = fmt.Sprintf("https://%s.ghe.com/%s", opts.Tenant, opts.Owner) + } else { + p.ExpectedExtensions.SourceRepositoryOwnerURI = fmt.Sprintf("https://github.com/%s", opts.Owner) + } + + // if issuer is anything other than the default, use the user-provided value; + // otherwise, select the appropriate default based on the tenant + if opts.Tenant != "" { + p.OIDCIssuer = fmt.Sprintf(verification.GitHubTenantOIDCIssuer, opts.Tenant) + } else { + p.OIDCIssuer = opts.OIDCIssuer + } + return p, nil } @@ -115,6 +136,16 @@ func (p *Policy) buildCertificateIdentityOption() (verify.PolicyOption, error) { return verify.WithCertificateIdentity(certId), nil } +func (p *Policy) VerifyPredicateType(a []*api.Attestation) ([]*api.Attestation, error) { + filteredAttestations := verification.FilterAttestations(p.ExpectedPredicateType, a) + + if len(filteredAttestations) == 0 { + return nil, fmt.Errorf("✗ No attestations found with predicate type: %s\n", p.ExpectedPredicateType) + } + + return filteredAttestations, nil +} + func (p *Policy) SigstorePolicy() (verify.PolicyBuilder, error) { artifactDigestPolicyOption, err := verification.BuildDigestPolicyOption(p.Artifact) if err != nil { @@ -130,10 +161,6 @@ func (p *Policy) SigstorePolicy() (verify.PolicyBuilder, error) { return policy, nil } -func addSchemeToRegex(s string) string { - return fmt.Sprintf("^https://%s", s) -} - func validateSignerWorkflow(opts *Options) (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 @@ -150,5 +177,55 @@ func validateSignerWorkflow(opts *Options) (string, error) { return "", errors.New("unknown host") } - return fmt.Sprintf("^https://%s/%s/%s", opts.Hostname, opts.SignerWorkflow), nil + return fmt.Sprintf("^https://%s/%s", opts.Hostname, opts.SignerWorkflow), nil +} + +func (p *Policy) VerifyCertExtensions(results []*verification.AttestationProcessingResult) error { + if len(results) == 0 { + return errors.New("no attestations proccessing results") + } + + var atLeastOneVerified bool + for _, attestation := range results { + if err := p.verifyCertExtensions(attestation); err != nil { + return err + } + atLeastOneVerified = true + } + + if atLeastOneVerified { + return nil + } else { + return verification.ErrNoAttestationsVerified + } +} + +func (p *Policy) verifyCertExtensions(attestation *verification.AttestationProcessingResult) error { + if p.ExpectedExtensions.SourceRepositoryOwnerURI != "" { + sourceRepositoryOwnerURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI + if !strings.EqualFold(p.ExpectedExtensions.SourceRepositoryOwnerURI, sourceRepositoryOwnerURI) { + return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", p.ExpectedExtensions.SourceRepositoryOwnerURI, sourceRepositoryOwnerURI) + } + } + + // if repo is set, check the SourceRepositoryURI field + if p.ExpectedExtensions.SourceRepositoryURI != "" { + sourceRepositoryURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryURI + if !strings.EqualFold(p.ExpectedExtensions.SourceRepositoryURI, sourceRepositoryURI) { + return fmt.Errorf("expected SourceRepositoryURI to be %s, got %s", p.ExpectedExtensions.SourceRepositoryURI, sourceRepositoryURI) + } + } + + if p.OIDCIssuer != "" { + certIssuer := attestation.VerificationResult.Signature.Certificate.Extensions.Issuer + if !strings.EqualFold(p.OIDCIssuer, certIssuer) { + if strings.Index(certIssuer, p.OIDCIssuer+"/") == 0 { + return fmt.Errorf("expected Issuer to be %s, got %s -- if you have a custom OIDC issuer policy for your enterprise, use the --cert-oidc-issuer flag with your expected issuer", p.OIDCIssuer, certIssuer) + } else { + return fmt.Errorf("expected Issuer to be %s, got %s", p.OIDCIssuer, certIssuer) + } + } + } + + return nil } diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index ae1e52955..70c3079e1 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -26,7 +26,7 @@ func TestBuildPolicy(t *testing.T) { SANRegex: "^https://github.com/sigstore/", } - _, err = buildVerifyPolicy(opts, *artifact) + _, err = newPolicy(opts, *artifact) require.NoError(t, err) } @@ -87,7 +87,6 @@ func TestValidateSignerWorkflow(t *testing.T) { workflowRegex, err := validateSignerWorkflow(opts) require.NoError(t, err) require.Equal(t, tc.expectedWorkflowRegex, workflowRegex) - } } diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 8b58296f3..a873d6c96 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -255,21 +255,9 @@ func runVerify(opts *Options) error { attestations = filteredAttestations } - policy, err := buildVerifyPolicy(opts, *artifact) + sigstoreResults, err := verifyAll(opts, *artifact, attestations) if err != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Failed to build verification policy")) - return err - } - - sigstoreRes := opts.SigstoreVerifier.Verify(attestations, policy) - if sigstoreRes.Error != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Verification failed")) - return sigstoreRes.Error - } - - // Verify extensions - if err := verification.VerifyCertExtensions(sigstoreRes.VerifyResults, opts.Tenant, opts.Owner, opts.Repo, opts.OIDCIssuer); err != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Verification failed")) + opts.Logger.Println(opts.Logger.ColorScheme.Red(err.Error())) return err } @@ -278,7 +266,7 @@ func runVerify(opts *Options) error { // If an exporter is provided with the --json flag, write the results to the terminal in JSON format if opts.exporter != nil { // print the results to the terminal as an array of JSON objects - if err = opts.exporter.Write(opts.Logger.IO, sigstoreRes.VerifyResults); err != nil { + if err = opts.exporter.Write(opts.Logger.IO, sigstoreResults.VerifyResults); err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Failed to write JSON output")) return err } @@ -288,7 +276,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(opts.Tenant, sigstoreRes.VerifyResults) + tableContent, err := buildTableVerifyContent(opts.Tenant, sigstoreResults.VerifyResults) if err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse results")) return err @@ -365,30 +353,26 @@ func buildTableVerifyContent(tenant string, results []*verification.AttestationP return content, nil } -func verifyAll(opts *Options, artifact artifact.DigestedArtifact, attestations []*api.Attestation) error { +func verifyAll(opts *Options, artifact artifact.DigestedArtifact, attestations []*api.Attestation) (*verification.SigstoreResults, error) { policy, err := newPolicy(opts, artifact) if err != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Failed to build verification policy")) - return err + return nil, fmt.Errorf("✗ Failed to build verification policy") } sp, err := policy.SigstorePolicy() if err != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Failed to build verification policy")) - return err + return nil, fmt.Errorf("✗ Failed to build Sigstore verification policy") } sigstoreRes := opts.SigstoreVerifier.Verify(attestations, sp) if sigstoreRes.Error != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Verification failed")) - return sigstoreRes.Error + return nil, fmt.Errorf("✗ Sigstore verification failed") } // Verify extensions if err := verification.VerifyCertExtensions(sigstoreRes.VerifyResults, opts.Tenant, opts.Owner, opts.Repo, opts.OIDCIssuer); err != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Verification failed")) - return err + return nil, fmt.Errorf("✗ Policy verification failed") } - return nil + return sigstoreRes, nil }