start moving over verify cmd and general verification code
Signed-off-by: Meredith Lancaster <malancas@github.com>
This commit is contained in:
parent
2ccc34afdc
commit
a082815d81
10 changed files with 1067 additions and 9 deletions
6
go.mod
6
go.mod
|
|
@ -14,9 +14,11 @@ require (
|
|||
github.com/cli/safeexec v1.0.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/distribution/reference v0.5.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/github/gh-attestation v0.3.1-0.20240213221736-bd06290fa8ee
|
||||
github.com/in-toto/in-toto-golang v0.9.0
|
||||
github.com/google/go-containerregistry v0.19.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
|
|
@ -33,6 +35,8 @@ require (
|
|||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278
|
||||
github.com/sigstore/protobuf-specs v0.3.0
|
||||
github.com/sigstore/sigstore-go v0.2.1-0.20240222221148-8bd2a8139edc
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.4
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
package attestation
|
||||
|
||||
import (
|
||||
"github.com/github/gh-attestation/cmd/app"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verify"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdAttestation(io *iostreams.IOStreams, version, buildDate string) *cobra.Command {
|
||||
func NewCmdAttestation(f *cmdutil.Factory) *cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "attestation",
|
||||
Short: "attestations",
|
||||
Use: "attestation [subcommand]",
|
||||
Short: "Work with attestations.",
|
||||
Aliases: []string{"at"},
|
||||
Long: heredoc.Docf(`
|
||||
Work with attestations that represent trusted metadata about artifacts and images.
|
||||
|
||||
The %[1]sattestation%[1]s command and all subcommands support the following account types:
|
||||
* Free tier
|
||||
* Pro tier
|
||||
* Team tier
|
||||
* GHEC
|
||||
* GHEC EMU
|
||||
`, "`"),
|
||||
}
|
||||
|
||||
root.AddCommand(app.NewDownloadCmd(io, version, buildDate))
|
||||
root.AddCommand(app.NewVerifyCmd(io, version, buildDate))
|
||||
root.AddCommand(app.NewTUFRootVerifyCmd(io))
|
||||
root.AddCommand(verify.NewVerifyCmd(f))
|
||||
|
||||
return root
|
||||
}
|
||||
|
|
|
|||
78
pkg/cmd/attestation/verification/policy.go
Normal file
78
pkg/cmd/attestation/verification/policy.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
|
||||
"github.com/sigstore/sigstore-go/pkg/verify"
|
||||
)
|
||||
|
||||
const (
|
||||
GitHubOIDCIssuer = "https://token.actions.githubusercontent.com"
|
||||
SLSAPredicateType = "https://slsa.dev/provenance/v1"
|
||||
// represents the GitHub hosted runner in the certificate RunnerEnvironment extension
|
||||
GitHubRunner = "github-hosted"
|
||||
)
|
||||
|
||||
func buildSANMatcher(opts *VerifyOpts) (verify.SubjectAlternativeNameMatcher, error) {
|
||||
if opts.SAN != "" || opts.SANRegex != "" {
|
||||
sanMatcher, err := verify.NewSANMatcher(opts.SAN, "", opts.SANRegex)
|
||||
if err != nil {
|
||||
return verify.SubjectAlternativeNameMatcher{}, err
|
||||
}
|
||||
return sanMatcher, nil
|
||||
}
|
||||
return verify.SubjectAlternativeNameMatcher{}, nil
|
||||
}
|
||||
|
||||
func buildCertificateIdentityOption(opts *VerifyOpts, runnerEnv string) (verify.PolicyOption, error) {
|
||||
sanMatcher, err := buildSANMatcher(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extensions := certificate.Extensions{
|
||||
Issuer: opts.OIDCIssuer,
|
||||
SourceRepositoryOwnerURI: fmt.Sprintf("https://github.com/%s", opts.Owner),
|
||||
RunnerEnvironment: runnerEnv,
|
||||
}
|
||||
certId, err := verify.NewCertificateIdentity(sanMatcher, extensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return verify.WithCertificateIdentity(certId), nil
|
||||
}
|
||||
|
||||
func buildVerifyCertIdOption(opts *VerifyOpts) (verify.PolicyOption, error) {
|
||||
if opts.DenySelfHostedRunner {
|
||||
withGHRunner, err := buildCertificateIdentityOption(opts, GitHubRunner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return withGHRunner, nil
|
||||
}
|
||||
|
||||
// if Extensions.RunnerEnvironment value is set to the empty string
|
||||
// through the second function argument,
|
||||
// no certificate matching will happen on the RunnerEnvironment field
|
||||
withAnyRunner, err := buildCertificateIdentityOption(opts, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return withAnyRunner, nil
|
||||
}
|
||||
|
||||
// BuildDigestPolicyOption builds a verify.ArtifactPolicyOption
|
||||
// from the given artifact digest and digest algorithm
|
||||
func BuildDigestPolicyOption(artifactDigest, digestAlgorithm string) (verify.ArtifactPolicyOption, error) {
|
||||
// sigstore-go expects the artifact digest to be decoded from hex
|
||||
decoded, err := hex.DecodeString(artifactDigest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return verify.WithArtifactDigest(digestAlgorithm, decoded), nil
|
||||
}
|
||||
27
pkg/cmd/attestation/verification/policy_test.go
Normal file
27
pkg/cmd/attestation/verification/policy_test.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/github/gh-attestation/pkg/artifact"
|
||||
"github.com/github/gh-attestation/pkg/github"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildPolicy(t *testing.T) {
|
||||
opts := &VerifyOpts{
|
||||
ArtifactPath: "../../test/data/public-good/sigstore-js-2.1.0.tgz",
|
||||
BundlePath: "../../test/data/public-good/sigstore-js-2.1.0-bundle.json",
|
||||
GitHubClient: github.NewTestClient(),
|
||||
OIDCIssuer: GitHubOIDCIssuer,
|
||||
SAN: "fake san",
|
||||
Owner: "github",
|
||||
}
|
||||
|
||||
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = buildVerifyPolicy(opts, artifact.Digest())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
199
pkg/cmd/attestation/verification/sigstore.go
Normal file
199
pkg/cmd/attestation/verification/sigstore.go
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/github/gh-attestation/pkg/github"
|
||||
"github.com/github/gh-attestation/pkg/output"
|
||||
|
||||
"github.com/sigstore/sigstore-go/pkg/bundle"
|
||||
"github.com/sigstore/sigstore-go/pkg/root"
|
||||
"github.com/sigstore/sigstore-go/pkg/tuf"
|
||||
"github.com/sigstore/sigstore-go/pkg/verify"
|
||||
)
|
||||
|
||||
const (
|
||||
PublicGoodIssuerOrg = "sigstore.dev"
|
||||
GitHubIssuerOrg = "GitHub, Inc."
|
||||
)
|
||||
|
||||
type SigstoreConfig struct {
|
||||
CustomTrustedRoot string
|
||||
Logger *output.Logger
|
||||
NoPublicGood bool
|
||||
}
|
||||
|
||||
type SigstoreVerifier struct {
|
||||
ghVerifier *verify.SignedEntityVerifier
|
||||
publicGoodVerifier *verify.SignedEntityVerifier
|
||||
customVerifier *verify.SignedEntityVerifier
|
||||
policy verify.PolicyBuilder
|
||||
onlyVerifyWithGithub bool
|
||||
Logger *output.Logger
|
||||
}
|
||||
|
||||
// NewSigstoreVerifier creates a new SigstoreVerifier struct
|
||||
// that is used to verify artifacts and attestations against the
|
||||
// Public Good, GitHub, or a custom trusted root.
|
||||
func NewSigstoreVerifier(config SigstoreConfig, policy verify.PolicyBuilder) (*SigstoreVerifier, error) {
|
||||
customVerifier, err := newCustomVerifier(config.CustomTrustedRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create custom verifier: %w", err)
|
||||
}
|
||||
|
||||
publicGoodVerifier, err := newPublicGoodVerifier()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Public Good Sigstore verifier: %w", err)
|
||||
}
|
||||
|
||||
ghVerifier, err := newGitHubVerifier()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GitHub Sigstore verifier: %w", err)
|
||||
}
|
||||
|
||||
return &SigstoreVerifier{
|
||||
ghVerifier: ghVerifier,
|
||||
publicGoodVerifier: publicGoodVerifier,
|
||||
customVerifier: customVerifier,
|
||||
Logger: config.Logger,
|
||||
policy: policy,
|
||||
onlyVerifyWithGithub: config.NoPublicGood,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *SigstoreVerifier) chooseVerifier(b *bundle.ProtobufBundle) (*verify.SignedEntityVerifier, string, error) {
|
||||
verifyContent, err := b.VerificationContent()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to get bundle verification content: %w", err)
|
||||
}
|
||||
leafCert, ok := verifyContent.HasCertificate()
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf("leaf cert not found")
|
||||
}
|
||||
if len(leafCert.Issuer.Organization) != 1 {
|
||||
return nil, "", fmt.Errorf("expected the leaf certificate issuer to only have one organization")
|
||||
}
|
||||
issuer := leafCert.Issuer.Organization[0]
|
||||
|
||||
// if user provided a custom trusted root file path, use the custom verifier
|
||||
if v.customVerifier != nil {
|
||||
return v.customVerifier, issuer, nil
|
||||
}
|
||||
|
||||
if v.onlyVerifyWithGithub {
|
||||
return v.ghVerifier, issuer, nil
|
||||
}
|
||||
|
||||
if leafCert.Issuer.Organization[0] == PublicGoodIssuerOrg {
|
||||
return v.publicGoodVerifier, issuer, nil
|
||||
} else if leafCert.Issuer.Organization[0] == GitHubIssuerOrg {
|
||||
return v.ghVerifier, issuer, nil
|
||||
}
|
||||
return nil, "", fmt.Errorf("leaf certificate issuer is not recognized")
|
||||
}
|
||||
|
||||
func (v *SigstoreVerifier) Verify(attestations []*github.Attestation) *SigstoreResults {
|
||||
// initialize the processing results before attempting to verify
|
||||
// with multiple verifiers
|
||||
results := make([]*AttestationProcessingResult, len(attestations))
|
||||
for i, att := range attestations {
|
||||
apr := &AttestationProcessingResult{
|
||||
Attestation: att,
|
||||
}
|
||||
results[i] = apr
|
||||
}
|
||||
|
||||
for i, apr := range results {
|
||||
v.Logger.VerbosePrintf("Verifying attestation #%d against the configured Sigstore trust roots\n", i+1)
|
||||
|
||||
// determine which verifier should attempt verification against the bundle
|
||||
verifier, issuer, err := v.chooseVerifier(apr.Attestation.Bundle)
|
||||
if err != nil {
|
||||
return &SigstoreResults{
|
||||
Error: fmt.Errorf("failed to find recognized issuer from bundle content: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
v.Logger.VerbosePrintf("Attempting verification against issuer \"%s\"...\n", issuer)
|
||||
// attempt to verify the attestation
|
||||
result, err := verifier.Verify(apr.Attestation.Bundle, v.policy)
|
||||
// if verification fails, create the error and exit verification early
|
||||
if err != nil {
|
||||
v.Logger.VerbosePrint(v.Logger.ColorScheme.Redf(
|
||||
"Failed to verify against issuer \"%s\" \n\n", issuer,
|
||||
))
|
||||
|
||||
return &SigstoreResults{
|
||||
Error: fmt.Errorf("verifying with issuer \"%s\": %w", issuer, err),
|
||||
}
|
||||
}
|
||||
|
||||
// if verification is successful, add the result
|
||||
// to the AttestationProcessingResult entry
|
||||
v.Logger.VerbosePrint(v.Logger.ColorScheme.Greenf(
|
||||
"SUCCESS - attestation signature verified with \"%s\"\n", issuer,
|
||||
))
|
||||
apr.VerificationResult = result
|
||||
}
|
||||
|
||||
return &SigstoreResults{
|
||||
VerifyResults: results,
|
||||
}
|
||||
}
|
||||
|
||||
func newCustomVerifier(trustedRootFilePath string) (*verify.SignedEntityVerifier, error) {
|
||||
if trustedRootFilePath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
trustedRoot, err := root.NewTrustedRootFromPath(trustedRootFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create trusted root from file %s: %w", trustedRootFilePath, err)
|
||||
}
|
||||
|
||||
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create custom verifier: %w", err)
|
||||
}
|
||||
|
||||
return gv, nil
|
||||
}
|
||||
|
||||
func newGitHubVerifier() (*verify.SignedEntityVerifier, error) {
|
||||
opts, err := GitHubTUFOptions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := tuf.New(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF client: %w", err)
|
||||
}
|
||||
trustedRoot, err := root.GetTrustedRoot(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GitHub verifier: %w", err)
|
||||
}
|
||||
|
||||
return gv, nil
|
||||
}
|
||||
|
||||
func newPublicGoodVerifier() (*verify.SignedEntityVerifier, error) {
|
||||
client, err := tuf.DefaultClient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF client: %w", err)
|
||||
}
|
||||
trustedRoot, err := root.GetTrustedRoot(client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get trusted root: %w", err)
|
||||
}
|
||||
|
||||
sv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Public Good verifier: %w", err)
|
||||
}
|
||||
|
||||
return sv, nil
|
||||
}
|
||||
129
pkg/cmd/attestation/verification/sigstore_test.go
Normal file
129
pkg/cmd/attestation/verification/sigstore_test.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/github/gh-attestation/pkg/artifact"
|
||||
"github.com/github/gh-attestation/pkg/artifact/oci"
|
||||
"github.com/github/gh-attestation/pkg/github"
|
||||
"github.com/github/gh-attestation/pkg/output"
|
||||
"github.com/github/gh-attestation/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var logger = output.NewDefaultLogger()
|
||||
|
||||
var publicGoodOpts = Options{
|
||||
ArtifactPath: test.NormalizeRelativePath("../../test/data/public-good/sigstore-js-2.1.0.tgz"),
|
||||
BundlePath: test.NormalizeRelativePath("../../test/data/public-good/sigstore-js-2.1.0-bundle.json"),
|
||||
DigestAlgorithm: "sha512",
|
||||
GitHubClient: github.NewTestClient(),
|
||||
Logger: logger,
|
||||
OIDCIssuer: GitHubOIDCIssuer,
|
||||
Owner: "sigstore",
|
||||
Limit: 30,
|
||||
OCIClient: oci.NewMockClient(),
|
||||
SANRegex: "^https://github.com/sigstore/",
|
||||
}
|
||||
|
||||
func TestVerify_PublicGoodSuccess(t *testing.T) {
|
||||
t.Skip()
|
||||
res := test.SuppressAndRestoreOutput()
|
||||
defer res()
|
||||
|
||||
artifact, err := artifact.NewDigestedArtifact(publicGoodOpts.OCIClient, publicGoodOpts.ArtifactPath, publicGoodOpts.DigestAlgorithm)
|
||||
require.NoError(t, err)
|
||||
|
||||
attestations, err := getAttestations(&publicGoodOpts, artifact.Digest())
|
||||
require.NoError(t, err)
|
||||
|
||||
policy, err := buildVerifyPolicy(&publicGoodOpts, artifact.Digest())
|
||||
require.NoError(t, err)
|
||||
|
||||
config := SigstoreConfig{
|
||||
CustomTrustedRoot: publicGoodOpts.CustomTrustedRoot,
|
||||
Logger: publicGoodOpts.Logger,
|
||||
NoPublicGood: publicGoodOpts.NoPublicGood,
|
||||
}
|
||||
|
||||
v, err := NewSigstoreVerifier(config, policy)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := v.Verify(attestations)
|
||||
assert.Nil(t, resp.Error)
|
||||
|
||||
verifyResults := resp.VerifyResults
|
||||
assert.Len(t, verifyResults, 1)
|
||||
|
||||
result := verifyResults[0]
|
||||
assert.NotNil(t, result.VerificationResult)
|
||||
assert.Equal(t, attestations[0], result.Attestation)
|
||||
}
|
||||
|
||||
func TestVerify_PublicGoodFail_WithNoPublicGoodFlag(t *testing.T) {
|
||||
t.Skip()
|
||||
res := test.SuppressAndRestoreOutput()
|
||||
defer res()
|
||||
|
||||
opts := publicGoodOpts
|
||||
opts.NoPublicGood = true
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.NoError(t, err)
|
||||
|
||||
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
|
||||
require.NoError(t, err)
|
||||
|
||||
attestations, err := getAttestations(&opts, artifact.Digest())
|
||||
require.NoError(t, err)
|
||||
|
||||
policy, err := buildVerifyPolicy(&publicGoodOpts, artifact.Digest())
|
||||
require.NoError(t, err)
|
||||
|
||||
config := SigstoreConfig{
|
||||
CustomTrustedRoot: opts.CustomTrustedRoot,
|
||||
Logger: opts.Logger,
|
||||
NoPublicGood: opts.NoPublicGood,
|
||||
}
|
||||
|
||||
v, err := NewSigstoreVerifier(config, policy)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := v.Verify(attestations)
|
||||
assert.Nil(t, resp.VerifyResults)
|
||||
assert.ErrorContains(t, resp.Error, "verifying with issuer \"sigstore.dev\"")
|
||||
}
|
||||
|
||||
func TestVerify_Failure(t *testing.T) {
|
||||
res := test.SuppressAndRestoreOutput()
|
||||
defer res()
|
||||
|
||||
opts := publicGoodOpts
|
||||
opts.DigestAlgorithm = "sha256"
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.NoError(t, err)
|
||||
|
||||
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
|
||||
require.NoError(t, err)
|
||||
|
||||
attestations, err := getAttestations(&opts, artifact.Digest())
|
||||
require.NoError(t, err)
|
||||
|
||||
policy, err := buildVerifyPolicy(&opts, artifact.Digest())
|
||||
require.NoError(t, err)
|
||||
|
||||
config := SigstoreConfig{
|
||||
CustomTrustedRoot: opts.CustomTrustedRoot,
|
||||
Logger: opts.Logger,
|
||||
NoPublicGood: opts.NoPublicGood,
|
||||
}
|
||||
|
||||
v, err := NewSigstoreVerifier(config, policy)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := v.Verify(attestations)
|
||||
assert.NotNil(t, resp.Error)
|
||||
assert.Nil(t, resp.VerifyResults)
|
||||
}
|
||||
156
pkg/cmd/attestation/verify/options.go
Normal file
156
pkg/cmd/attestation/verify/options.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-attestation/pkg/artifact/digest"
|
||||
"github.com/github/gh-attestation/pkg/artifact/oci"
|
||||
"github.com/github/gh-attestation/pkg/github"
|
||||
"github.com/github/gh-attestation/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
// OfflineMode is used when the user provides a bundle path
|
||||
OfflineMode = "offline"
|
||||
// OnlineMode is used when the user does not provide a bundle path
|
||||
// An owner or repo scope is used to fetch attestations from GitHub
|
||||
OnlineMode = "online"
|
||||
)
|
||||
|
||||
// Options captures the options for the verify command
|
||||
type Options struct {
|
||||
ArtifactPath string
|
||||
BundlePath string
|
||||
CustomTrustedRoot string
|
||||
DenySelfHostedRunner bool
|
||||
DigestAlgorithm string
|
||||
JsonResult bool
|
||||
NoPublicGood bool
|
||||
OIDCIssuer string
|
||||
Owner string
|
||||
Quiet bool
|
||||
Repo string
|
||||
SAN string
|
||||
SANRegex string
|
||||
Verbose bool
|
||||
GitHubClient github.Client
|
||||
Logger *output.Logger
|
||||
Limit int
|
||||
OCIClient oci.Client
|
||||
}
|
||||
|
||||
// Mode returns a string indicating either online or offline mode
|
||||
func (opts *Options) Mode() string {
|
||||
if opts.BundlePath == "" {
|
||||
return OnlineMode
|
||||
}
|
||||
return OfflineMode
|
||||
}
|
||||
|
||||
// ConfigureGitHubClient configures a live GitHub client if the tool
|
||||
// is running in online mode
|
||||
func (opts *Options) ConfigureGitHubClient(version, date string) error {
|
||||
if opts.Mode() == OfflineMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := github.NewLiveClient(version, date)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.GitHubClient = client
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigureOCIClient configures an OCI client
|
||||
func (opts *Options) ConfigureOCIClient() {
|
||||
opts.OCIClient = oci.NewLiveClient()
|
||||
}
|
||||
|
||||
// ConfigureLogger configures a logger using configuration provided
|
||||
// through the options
|
||||
func (opts *Options) ConfigureLogger() {
|
||||
opts.Logger = output.NewLogger(opts.Quiet, opts.Verbose)
|
||||
}
|
||||
|
||||
// Clean cleans the file path option values
|
||||
func (opts *Options) Clean() {
|
||||
if opts.BundlePath != "" {
|
||||
opts.BundlePath = filepath.Clean(opts.BundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *Options) SetPolicyFlags() {
|
||||
// check that Repo is in the expected format if provided
|
||||
if opts.Repo != "" {
|
||||
// we expect the repo argument to be in the format <OWNER>/<REPO>
|
||||
splitRepo := strings.Split(opts.Repo, "/")
|
||||
|
||||
// if Repo is provided but owner is not, set the OWNER portion of the Repo value
|
||||
// to Owner
|
||||
opts.Owner = splitRepo[0]
|
||||
|
||||
if opts.SAN == "" && opts.SANRegex == "" {
|
||||
opts.SANRegex = expandToGitHubURL(opts.Repo)
|
||||
}
|
||||
return
|
||||
}
|
||||
if opts.SAN == "" && opts.SANRegex == "" {
|
||||
opts.SANRegex = expandToGitHubURL(opts.Owner)
|
||||
}
|
||||
}
|
||||
|
||||
// AreFlagsValid checks that the provided flag combination is valid
|
||||
// and returns an error otherwise
|
||||
func (opts *Options) AreFlagsValid() error {
|
||||
// either BundlePath or Repo must be set to configure offline or online mode
|
||||
if opts.BundlePath == "" && opts.Repo == "" && opts.Owner == "" {
|
||||
return fmt.Errorf("either bundle or repo or owner must be provided")
|
||||
}
|
||||
|
||||
// DigestAlgorithm must not be empty
|
||||
if opts.DigestAlgorithm == "" {
|
||||
return fmt.Errorf("digest-alg cannot be empty")
|
||||
}
|
||||
|
||||
if !digest.IsValidDigestAlgorithm(opts.DigestAlgorithm) {
|
||||
return fmt.Errorf("invalid digtest algorithm '%s' provided in digest-alg", opts.DigestAlgorithm)
|
||||
}
|
||||
|
||||
// OIDCIssuer must not be empty
|
||||
if opts.OIDCIssuer == "" {
|
||||
return fmt.Errorf("cert-oidc-issuer cannot be empty")
|
||||
}
|
||||
|
||||
// either Owner or Repo must be supplied
|
||||
if opts.Owner == "" && opts.Repo == "" {
|
||||
return fmt.Errorf("owner or repo must be provided")
|
||||
}
|
||||
|
||||
// SAN or SAN regex are mutually exclusive, only one can be provided
|
||||
if opts.SAN != "" && opts.SANRegex != "" {
|
||||
return fmt.Errorf("cert-identity and cert-identity-regex cannot both be provided")
|
||||
}
|
||||
|
||||
// check that Repo is in the expected format if provided
|
||||
if opts.Repo != "" {
|
||||
// we expect the repo argument to be in the format <OWNER>/<REPO>
|
||||
splitRepo := strings.Split(opts.Repo, "/")
|
||||
if len(splitRepo) != 2 {
|
||||
return fmt.Errorf("invalid value provided for repo: %s", opts.Repo)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that limit is between 1 and 1000
|
||||
if opts.Limit < 1 || opts.Limit > 1000 {
|
||||
return fmt.Errorf("limit %d not allowed, must be between 1 and 1000", opts.Limit)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func expandToGitHubURL(ownerOrRepo string) string {
|
||||
return fmt.Sprintf("^https://github.com/%s/", ownerOrRepo)
|
||||
}
|
||||
229
pkg/cmd/attestation/verify/options_test.go
Normal file
229
pkg/cmd/attestation/verify/options_test.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/github/gh-attestation/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
publicGoodArtifactPath = test.NormalizeRelativePath("../../test/data/public-good/sigstore-js-2.1.0.tgz")
|
||||
publicGoodBundlePath = test.NormalizeRelativePath("../../test/data/public-good/sigstore-js-2.1.0-bundle.json")
|
||||
)
|
||||
|
||||
func TestAreFlagsValid(t *testing.T) {
|
||||
t.Run("missing BundlePath, Repo, and Owner", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "either bundle or repo or owner must be provided")
|
||||
})
|
||||
|
||||
t.Run("missing DigestAlgorithm", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
OIDCIssuer: "some issuer",
|
||||
Owner: "sigstore",
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "digest-alg cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("missing Owner and Repo", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "owner or repo must be provided")
|
||||
})
|
||||
|
||||
t.Run("has both SAN and SANRegex", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
Owner: "sigstore",
|
||||
SAN: "some san",
|
||||
SANRegex: "^some san regex$",
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "cert-identity and cert-identity-regex cannot both be provided")
|
||||
})
|
||||
|
||||
t.Run("has invalid Repo value", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
Repo: "sigstoresigstore-js",
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "invalid value provided for repo")
|
||||
})
|
||||
|
||||
t.Run("missing OIDCIssuer", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Owner: "sigstore",
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "cert-oidc-issuer cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("invalid limit < 0", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Owner: "sigstore",
|
||||
OIDCIssuer: "some issuer",
|
||||
Limit: 0,
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "limit 0 not allowed, must be between 1 and 1000")
|
||||
})
|
||||
|
||||
t.Run("invalid limit > 1000", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Owner: "sigstore",
|
||||
OIDCIssuer: "some issuer",
|
||||
Limit: 1001,
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "limit 1001 not allowed, must be between 1 and 1000")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetPolicyFlags(t *testing.T) {
|
||||
t.Run("sets Owner and SANRegex when Repo is provided", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
Repo: "sigstore/sigstore-js",
|
||||
}
|
||||
|
||||
opts.SetPolicyFlags()
|
||||
assert.Equal(t, "sigstore", opts.Owner)
|
||||
assert.Equal(t, "sigstore/sigstore-js", opts.Repo)
|
||||
assert.Equal(t, "^https://github.com/sigstore/sigstore-js/", opts.SANRegex)
|
||||
})
|
||||
|
||||
t.Run("does not set SANRegex when SANRegex and Repo are provided", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
Repo: "sigstore/sigstore-js",
|
||||
SANRegex: "^https://github/foo",
|
||||
}
|
||||
|
||||
opts.SetPolicyFlags()
|
||||
assert.Equal(t, "sigstore", opts.Owner)
|
||||
assert.Equal(t, "sigstore/sigstore-js", opts.Repo)
|
||||
assert.Equal(t, "^https://github/foo", opts.SANRegex)
|
||||
})
|
||||
|
||||
t.Run("sets SANRegex when Owner is provided", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
Owner: "sigstore",
|
||||
}
|
||||
|
||||
opts.SetPolicyFlags()
|
||||
assert.Equal(t, "sigstore", opts.Owner)
|
||||
assert.Equal(t, "^https://github.com/sigstore/", opts.SANRegex)
|
||||
})
|
||||
|
||||
t.Run("does not set SANRegex when SANRegex and Owner are provided", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
Owner: "sigstore",
|
||||
SANRegex: "^https://github/foo",
|
||||
}
|
||||
|
||||
opts.SetPolicyFlags()
|
||||
assert.Equal(t, "sigstore", opts.Owner)
|
||||
assert.Equal(t, "^https://github/foo", opts.SANRegex)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClean(t *testing.T) {
|
||||
t.Skip()
|
||||
validBundlePath := "foo/attestation.json"
|
||||
opts := &Options{
|
||||
BundlePath: validBundlePath,
|
||||
}
|
||||
|
||||
opts.Clean()
|
||||
assert.Equal(t, validBundlePath, opts.BundlePath)
|
||||
}
|
||||
|
||||
func TestMode(t *testing.T) {
|
||||
t.Run("run in offline mode when bundle is provided", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
}
|
||||
|
||||
assert.Equal(t, OfflineMode, opts.Mode())
|
||||
})
|
||||
|
||||
t.Run("run in offline mode when bundle and repo are provided", func(t *testing.T) {
|
||||
opts := &Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Repo: "sigstore/sigstore-js",
|
||||
}
|
||||
|
||||
assert.Equal(t, OfflineMode, opts.Mode())
|
||||
})
|
||||
|
||||
t.Run("run in online mode when repo are provided", func(t *testing.T) {
|
||||
opts := &Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Repo: "sigstore/sigstore-js",
|
||||
}
|
||||
|
||||
assert.Equal(t, OnlineMode, opts.Mode())
|
||||
})
|
||||
}
|
||||
22
pkg/cmd/attestation/verify/policy.go
Normal file
22
pkg/cmd/attestation/verify/policy.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"github.com/sigstore/sigstore-go/pkg/verify"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
)
|
||||
|
||||
func buildVerifyPolicy(opts *Options, artifactDigest string) (verify.PolicyBuilder, error) {
|
||||
artifactDigestPolicyOption, err := verification.BuildDigestPolicyOption(artifactDigest, opts.DigestAlgorithm)
|
||||
if err != nil {
|
||||
return verify.PolicyBuilder{}, err
|
||||
}
|
||||
|
||||
certIdOption, err := buildVerifyCertIdOption(opts)
|
||||
if err != nil {
|
||||
return verify.PolicyBuilder{}, err
|
||||
}
|
||||
|
||||
policy := verify.NewPolicy(artifactDigestPolicyOption, certIdOption)
|
||||
return policy, nil
|
||||
}
|
||||
205
pkg/cmd/attestation/verify/verify.go
Normal file
205
pkg/cmd/attestation/verify/verify.go
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/pkg/github"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewVerifyCmd(f *cmdutil.Factory) *cobra.Command {
|
||||
opts := &verify.Options{}
|
||||
verifyCmd := &cobra.Command{
|
||||
Use: "verify <artifact-path-or-url>",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Cryptographically verify an artifact",
|
||||
Long: heredoc.Docf(`
|
||||
Cryptographically verify the authenticity of an artifact using the
|
||||
associated trusted metadata.
|
||||
|
||||
The command accepts either:
|
||||
* a relative path to a local artifact
|
||||
* a container image URI (e.g. oci://<my-OCI-image-URI>)
|
||||
|
||||
Note that you must already be authenticated with a container registry
|
||||
if you provide an OCI image URI as the artifact.
|
||||
|
||||
The command also requires you provide either the %[1]s--owner%[1]s
|
||||
or %[1]s--repo%[1]s flag.
|
||||
The value of the %[1]s--owner%[1]s flag should be the name of the GitHub organization
|
||||
that the artifact is associated with.
|
||||
The value of the %[1]s--repo%[1]s flag should be the name of the GitHub repository that
|
||||
the artifact is associated with.
|
||||
|
||||
By default, the command will verify the artifact against trusted metadata stored remotely.
|
||||
If you would like to verify the artifact against local metadata,
|
||||
you can provide a path to the local trusted metadata bundle file with the
|
||||
%[1]s--bundle%[1]s flag.
|
||||
|
||||
By default, the command will use the SHA-256 hash algorithm to create the artifact digest
|
||||
used for verification.
|
||||
You can specify the SHA-512 algorithm instead using the %[1]s--digest-alg%[1]s flag.
|
||||
|
||||
If the %[1]s--json-result%[1]s flag is provided, the command will print the verification
|
||||
results as JSON.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
# Verify a local artifact with the repository name
|
||||
$ gh attestation verify <my-artifact> --repo <repo-name>
|
||||
|
||||
# Verify a local artifact with the organization name
|
||||
$ gh attestation verify <my-artifact> --owner <owner>
|
||||
|
||||
# Verify an OCI image using a local trusted metadata bundle
|
||||
$ gh attestation verify oci://<my-OCI-image> --owner <owner> --bundle <path-to-bundle>
|
||||
`),
|
||||
// PreRunE is used to validate flags before the command is run
|
||||
// If an error is returned, its message will be printed to the terminal
|
||||
// along with information about how use the command
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Create a logger for use throughout the verify command
|
||||
opts.ConfigureLogger()
|
||||
|
||||
// Configure the live OCI client
|
||||
opts.ConfigureOCIClient()
|
||||
|
||||
// set the artifact path
|
||||
opts.ArtifactPath = args[0]
|
||||
|
||||
// Check that the given flag combination is valid
|
||||
if err := opts.AreFlagsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clean file path options
|
||||
opts.Clean()
|
||||
|
||||
// set policy flags based on what has been provided
|
||||
opts.SetPolicyFlags()
|
||||
|
||||
return nil
|
||||
},
|
||||
// Use Run instead of RunE because if an error is returned by RunVerify
|
||||
// when RunE is used, the command usage will be printed
|
||||
// We only want to print the error, not usage
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Configure the GitHub API client
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
if err := runVerify(opts); err != nil {
|
||||
opts.Logger.Println(opts.Logger.ColorScheme.Redf("Failed to verify the artifact: %s", err.Error()))
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// general flags
|
||||
verifyCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles")
|
||||
verifyCmd.Flags().StringVarP(&opts.DigestAlgorithm, "digest-alg", "d", "sha256", "The algorithm used to compute a digest of the artifact (sha256 or sha512)")
|
||||
verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by")
|
||||
verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format <owner>/<repo>")
|
||||
verifyCmd.MarkFlagsMutuallyExclusive("owner", "repo")
|
||||
verifyCmd.Flags().BoolVarP(&opts.NoPublicGood, "no-public-good", "", false, "Only verify attestations signed with GitHub's Sigstore instance")
|
||||
verifyCmd.Flags().BoolVarP(&opts.JsonResult, "json-result", "j", false, "Output verification result as JSON lines")
|
||||
verifyCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "If set to true, the CLI will not print any diagnostic logging.")
|
||||
verifyCmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "If set to true, the CLI will print verbose diagnostic logging.")
|
||||
verifyCmd.MarkFlagsMutuallyExclusive("quiet", "verbose")
|
||||
verifyCmd.Flags().StringVarP(&opts.CustomTrustedRoot, "custom-trusted-root", "", "", "Path to a custom trustedroot.json file to use for verification")
|
||||
verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", github.DefaultLimit, "Maximum number of attestations to fetch")
|
||||
// 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.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex")
|
||||
verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verify.GitHubOIDCIssuer, "Issuer of the OIDC token")
|
||||
|
||||
return verifyCmd
|
||||
}
|
||||
|
||||
func runVerify(opts *Options) error {
|
||||
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to digest artifact: %s", err)
|
||||
}
|
||||
|
||||
opts.Logger.Printf("Verifying attestations for the artifact found at %s\n", artifact.URL)
|
||||
|
||||
attestations, err := getAttestations(opts, artifact.DigestWithAlg())
|
||||
if err != nil {
|
||||
if ok := errors.Is(err, github.ErrNoAttestations{}); ok {
|
||||
return fmt.Errorf("no attestations found for subject: %s", artifact.DigestWithAlg())
|
||||
}
|
||||
return fmt.Errorf("failed to fetch attestations for subject: %s", artifact.DigestWithAlg())
|
||||
}
|
||||
|
||||
policy, err := buildVerifyPolicy(opts, artifact.Digest())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build policy: %w", err)
|
||||
}
|
||||
|
||||
config := SigstoreConfig{
|
||||
CustomTrustedRoot: opts.CustomTrustedRoot,
|
||||
Logger: opts.Logger,
|
||||
NoPublicGood: opts.NoPublicGood,
|
||||
}
|
||||
|
||||
sv, err := NewSigstoreVerifier(config, policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sigstoreRes := sv.Verify(attestations)
|
||||
if sigstoreRes.Error != nil {
|
||||
return fmt.Errorf("at least one attestation failed to verify against Sigstore: %w", sigstoreRes.Error)
|
||||
}
|
||||
|
||||
opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green(
|
||||
"Successfully verified all attestations against Sigstore!\n\n",
|
||||
))
|
||||
|
||||
// Try verifying the attestation's predicate type against the expect SLSA predicate type
|
||||
if err = verifySLSAPredicateType(opts.Logger, sigstoreRes.VerifyResults); err != nil {
|
||||
return fmt.Errorf("at least one attestation failed to verify predicate type verification: %w", err)
|
||||
}
|
||||
|
||||
opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green("Successfully verified the SLSA predicate type of all attestations!\n"))
|
||||
|
||||
opts.Logger.Println(opts.Logger.ColorScheme.Green("All attestations have been successfully verified!"))
|
||||
|
||||
if opts.JsonResult {
|
||||
verificationResults := sigstoreRes.VerifyResults
|
||||
// print each result as JSON line
|
||||
|
||||
jsonResults := make([]string, len(verificationResults))
|
||||
for i, verificationResult := range verificationResults {
|
||||
jsonBytes, err := json.Marshal(verificationResult)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create JSON output")
|
||||
}
|
||||
|
||||
jsonResults[i] = string(jsonBytes)
|
||||
}
|
||||
|
||||
rows := make([][]string, 1)
|
||||
rows[0] = jsonResults
|
||||
opts.Logger.PrintTableToStdOut(nil, rows)
|
||||
}
|
||||
|
||||
// All attestations passed verification and policy evaluation
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifySLSAPredicateType(logger *output.Logger, apr []*AttestationProcessingResult) error {
|
||||
logger.VerbosePrintf("Evaluating attestations have valid SLSA predicate type...\n")
|
||||
|
||||
for _, result := range apr {
|
||||
if result.VerificationResult.Statement.PredicateType != SLSAPredicateType {
|
||||
return ErrNoMatchingSLSAPredicate
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue