Update verification results printing by malancas · Pull Request #9937 · cli/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions pkg/cmd/attestation/verification/extensions.go
21 changes: 14 additions & 7 deletions pkg/cmd/attestation/verification/extensions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,19 @@ func TestVerifyCertExtensions(t *testing.T) {
}

t.Run("passes with one result", func(t *testing.T) {
err := VerifyCertExtensions(results, c)
verified, err := VerifyCertExtensions(results, c)
require.NoError(t, err)
require.Len(t, verified, 1)
})

t.Run("passes with 1/2 valid results", func(t *testing.T) {
twoResults := []*AttestationProcessingResult{createSampleResult(), createSampleResult()}
require.Len(t, twoResults, 2)
twoResults[1].VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI = "https://github.com/wrong"

err := VerifyCertExtensions(twoResults, c)
verified, err := VerifyCertExtensions(twoResults, c)
require.NoError(t, err)
require.Len(t, verified, 1)
})

t.Run("fails when all results fail verification", func(t *testing.T) {
Expand All @@ -56,35 +58,40 @@ func TestVerifyCertExtensions(t *testing.T) {
twoResults[0].VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI = "https://github.com/wrong"
twoResults[1].VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI = "https://github.com/wrong"

err := VerifyCertExtensions(twoResults, c)
verified, err := VerifyCertExtensions(twoResults, c)
require.Error(t, err)
require.Nil(t, verified)
})

t.Run("with wrong SourceRepositoryOwnerURI", func(t *testing.T) {
expectedCriteria := c
expectedCriteria.Certificate.SourceRepositoryOwnerURI = "https://github.com/wrong"
err := VerifyCertExtensions(results, expectedCriteria)
verified, err := VerifyCertExtensions(results, expectedCriteria)
require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://github.com/wrong, got https://github.com/owner")
require.Nil(t, verified)
})

t.Run("with wrong SourceRepositoryURI", func(t *testing.T) {
expectedCriteria := c
expectedCriteria.Certificate.SourceRepositoryURI = "https://github.com/foo/wrong"
err := VerifyCertExtensions(results, expectedCriteria)
verified, err := VerifyCertExtensions(results, expectedCriteria)
require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://github.com/foo/wrong, got https://github.com/owner/repo")
require.Nil(t, verified)
})

t.Run("with wrong OIDCIssuer", func(t *testing.T) {
expectedCriteria := c
expectedCriteria.Certificate.Issuer = "wrong"
err := VerifyCertExtensions(results, expectedCriteria)
verified, err := VerifyCertExtensions(results, expectedCriteria)
require.ErrorContains(t, err, "expected Issuer to be wrong, got https://token.actions.githubusercontent.com")
require.Nil(t, verified)
})

t.Run("with partial OIDCIssuer match", func(t *testing.T) {
expectedResults := results
expectedResults[0].VerificationResult.Signature.Certificate.Extensions.Issuer = "https://token.actions.githubusercontent.com/foo-bar"
err := VerifyCertExtensions(expectedResults, c)
verified, err := VerifyCertExtensions(expectedResults, c)
require.ErrorContains(t, err, "expected Issuer to be https://token.actions.githubusercontent.com, got https://token.actions.githubusercontent.com/foo-bar -- if you have a custom OIDC issuer")
require.Nil(t, verified)
})
}
54 changes: 50 additions & 4 deletions pkg/cmd/attestation/verification/mock_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@ import (

"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
"github.com/sigstore/sigstore-go/pkg/bundle"
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"

in_toto "github.com/in-toto/attestation/go/v1"
"github.com/sigstore/sigstore-go/pkg/verify"
)

type MockSigstoreVerifier struct {
t *testing.T
t *testing.T
mockResults []*AttestationProcessingResult
}

func (v *MockSigstoreVerifier) Verify(attestations []*api.Attestation, policy verify.PolicyBuilder) ([]*AttestationProcessingResult, error) {
func (v *MockSigstoreVerifier) Verify([]*api.Attestation, verify.PolicyBuilder) ([]*AttestationProcessingResult, error) {
if v.mockResults != nil {
return v.mockResults, nil
}

statement := &in_toto.Statement{}
statement.PredicateType = SLSAPredicateV1

Expand Down Expand Up @@ -45,11 +51,51 @@ func (v *MockSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve
}

func NewMockSigstoreVerifier(t *testing.T) *MockSigstoreVerifier {
return &MockSigstoreVerifier{t}
result := BuildSigstoreJsMockResult(t)
results := []*AttestationProcessingResult{&result}

return &MockSigstoreVerifier{t, results}
}

func NewMockSigstoreVerifierWithMockResults(t *testing.T, mockResults []*AttestationProcessingResult) *MockSigstoreVerifier {
return &MockSigstoreVerifier{t, mockResults}
}

type FailSigstoreVerifier struct{}

func (v *FailSigstoreVerifier) Verify(attestations []*api.Attestation, policy verify.PolicyBuilder) ([]*AttestationProcessingResult, error) {
func (v *FailSigstoreVerifier) Verify([]*api.Attestation, verify.PolicyBuilder) ([]*AttestationProcessingResult, error) {
return nil, fmt.Errorf("failed to verify attestations")
}

func BuildMockResult(b *bundle.Bundle, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer string) AttestationProcessingResult {
statement := &in_toto.Statement{}
statement.PredicateType = SLSAPredicateV1

return AttestationProcessingResult{
Attestation: &api.Attestation{
Bundle: b,
},
VerificationResult: &verify.VerificationResult{
Statement: statement,
Signature: &verify.SignatureVerificationResult{
Certificate: &certificate.Summary{
Extensions: certificate.Extensions{
BuildSignerURI: buildSignerURI,
SourceRepositoryOwnerURI: sourceRepoOwnerURI,
SourceRepositoryURI: sourceRepoURI,
Issuer: issuer,
},
},
},
},
}
}

func BuildSigstoreJsMockResult(t *testing.T) AttestationProcessingResult {
bundle := data.SigstoreBundle(t)
buildSignerURI := "https://github.com/github/example/.github/workflows/release.yml@refs/heads/main"
sourceRepoOwnerURI := "https://github.com/sigstore"
sourceRepoURI := "https://github.com/sigstore/sigstore-js"
issuer := "https://token.actions.githubusercontent.com"
return BuildMockResult(bundle, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer)
}
23 changes: 23 additions & 0 deletions pkg/cmd/attestation/verify/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,26 @@ func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestatio
msg := fmt.Sprintf("Loaded %s from GitHub API", pluralAttestation)
return attestations, msg, nil
}

func verifyAttestations(art artifact.DigestedArtifact, att []*api.Attestation, sgVerifier verification.SigstoreVerifier, ec verification.EnforcementCriteria) ([]*verification.AttestationProcessingResult, string, error) {
sgPolicy, err := buildSigstoreVerifyPolicy(ec, art)
if err != nil {
logMsg := "✗ Failed to build Sigstore verification policy"
return nil, logMsg, err
}

sigstoreVerified, err := sgVerifier.Verify(att, sgPolicy)
if err != nil {
logMsg := "✗ Sigstore verification failed"
return nil, logMsg, err
}

// Verify extensions
certExtVerified, err := verification.VerifyCertExtensions(sigstoreVerified, ec)
if err != nil {
logMsg := "✗ Policy verification failed"
return nil, logMsg, err
}

return certExtVerified, "", nil
}
117 changes: 117 additions & 0 deletions pkg/cmd/attestation/verify/attestation_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//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"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
"github.com/stretchr/testify/require"
)

func getAttestationsFor(t *testing.T, bundlePath string) []*api.Attestation {
t.Helper()

attestations, err := verification.GetLocalAttestations(bundlePath)
require.NoError(t, err)

return attestations
}

func TestVerifyAttestations(t *testing.T) {
sgVerifier := verification.NewLiveSigstoreVerifier(verification.SigstoreConfig{
Logger: io.NewTestHandler(),
})

certSummary := certificate.Summary{}
certSummary.SourceRepositoryOwnerURI = "https://github.com/sigstore"
certSummary.SourceRepositoryURI = "https://github.com/sigstore/sigstore-js"
certSummary.Issuer = verification.GitHubOIDCIssuer

ec := verification.EnforcementCriteria{
Certificate: certSummary,
PredicateType: verification.SLSAPredicateV1,
SANRegex: "^https://github.com/sigstore/",
}
require.NoError(t, ec.Valid())

artifactPath := test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz")
a, err := artifact.NewDigestedArtifact(nil, artifactPath, "sha512")
require.NoError(t, err)

t.Run("all attestations pass verification", func(t *testing.T) {
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
require.Len(t, attestations, 2)
results, errMsg, err := verifyAttestations(*a, attestations, sgVerifier, ec)
require.NoError(t, err)
require.Zero(t, errMsg)
require.Len(t, results, 2)
})

t.Run("passes verification with 2/3 attestations passing Sigstore verification", func(t *testing.T) {
invalidBundle := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json")
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
attestations = append(attestations, invalidBundle[0])
require.Len(t, attestations, 3)

results, errMsg, err := verifyAttestations(*a, attestations, sgVerifier, ec)
require.NoError(t, err)
require.Zero(t, errMsg)
require.Len(t, results, 2)
})

t.Run("fails verification when Sigstore verification fails", func(t *testing.T) {
invalidBundle := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json")
invalidBundle2 := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json")
attestations := append(invalidBundle, invalidBundle2...)
require.Len(t, attestations, 2)

results, errMsg, err := verifyAttestations(*a, attestations, sgVerifier, ec)
require.Error(t, err)
require.Contains(t, errMsg, "✗ Sigstore verification failed")
require.Nil(t, results)
})

t.Run("attestations fail to verify when cert extensions don't match enforcement criteria", func(t *testing.T) {
sgjAttestation := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
reusableWorkflowAttestations := getAttestationsFor(t, "../test/data/reusable-workflow-attestation.sigstore.json")
attestations := []*api.Attestation{sgjAttestation[0], reusableWorkflowAttestations[0], sgjAttestation[1]}
require.Len(t, attestations, 3)

rwfResult := verification.BuildMockResult(reusableWorkflowAttestations[0].Bundle, "", "https://github.com/malancas", "", verification.GitHubOIDCIssuer)
sgjResult := verification.BuildSigstoreJsMockResult(t)
mockResults := []*verification.AttestationProcessingResult{&sgjResult, &rwfResult, &sgjResult}
mockSgVerifier := verification.NewMockSigstoreVerifierWithMockResults(t, mockResults)

// we want to test that attestations that pass Sigstore verification but fail
// cert extension verification are filtered out properly in the second step
// in verifyAttestations. By using a mock Sigstore verifier, we can ensure
// that the call to verification.VerifyCertExtensions in verifyAttestations
// is filtering out attestations as expected
results, errMsg, err := verifyAttestations(*a, attestations, mockSgVerifier, ec)
require.NoError(t, err)
require.Zero(t, errMsg)
require.Len(t, results, 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally i'd like to check that the rwfResult is specifically the one being excluded. can we do an array comparison for sgjAttestation[0] and sgjAttestation[1]?

for _, result := range results {
require.NotEqual(t, result.Attestation.Bundle, reusableWorkflowAttestations[0].Bundle)
}
})

t.Run("fails verification when cert extension verification fails", func(t *testing.T) {
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
require.Len(t, attestations, 2)

expectedCriteria := ec
expectedCriteria.Certificate.SourceRepositoryOwnerURI = "https://github.com/wrong"

results, errMsg, err := verifyAttestations(*a, attestations, sgVerifier, expectedCriteria)
require.Error(t, err)
require.Contains(t, errMsg, "✗ Policy verification failed")
require.Nil(t, results)
})
}
20 changes: 4 additions & 16 deletions pkg/cmd/attestation/verify/verify.go