cli/pkg/cmd/attestation/verification/sigstore.go
Meredith Lancaster 90b7bf97c5
gh-attestation cmd integration (#8698)
* add attestation cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add codeowners

Signed-off-by: Meredith Lancaster <malancas@github.com>

* update args passed to the attestation cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename file

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use gh-attestation branch for passing iostreams from the root

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add package security team entry to codeowners

Signed-off-by: Meredith Lancaster <malancas@github.com>

* start moving over verify cmd and general verification code

Signed-off-by: Meredith Lancaster <malancas@github.com>

* clean up common and verify specific policy code

Signed-off-by: Meredith Lancaster <malancas@github.com>

* move artifact package over

Signed-off-by: Meredith Lancaster <malancas@github.com>

* start pulling in the github api client wrapper

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix imports

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add logger and test packages

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add additional packages to support verify command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix mock api client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* clean up mock api client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* include missing fields

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use correct owner

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add more mock api client options

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add download cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add inspect cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* pass factory object to inspect cmd, add inspect sub cmd to attestation cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add verify-tuf-root cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* pass iostream struct from command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename logger pkg to logger

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix path in codeowners

Signed-off-by: Meredith Lancaster <malancas@github.com>

* formatter

Signed-off-by: Meredith Lancaster <malancas@github.com>

* go mod tidy

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix printf linter issue

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix printf linter issue

Signed-off-by: Meredith Lancaster <malancas@github.com>

* check user's GH host for compatibility

Signed-off-by: Meredith Lancaster <malancas@github.com>

* pass oci client to commands directly

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* mark tuf-root-verify cmd hidden

Signed-off-by: Meredith Lancaster <malancas@github.com>

* move client initialization back to subcommands

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add more verbose options and logging

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add missing logger

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add testing around OCI and API client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add integration test

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix file path

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* build executable before integration test

Signed-off-by: Meredith Lancaster <malancas@github.com>

* split integration tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove integration test steps

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix flag value

Signed-off-by: Meredith Lancaster <malancas@github.com>

* run integration tests on ubuntu for now

Signed-off-by: Meredith Lancaster <malancas@github.com>

* pull over doc updates

Signed-off-by: Meredith Lancaster <malancas@github.com>

* delete unused test data

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove Go patch version

Signed-off-by: Meredith Lancaster <malancas@github.com>

* switch assert to require

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename file

Signed-off-by: Meredith Lancaster <malancas@github.com>

* move integration tests to prexisting test workflow

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use platform matrix for integration tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* simplify build step

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use StringEnumFlag handling

Signed-off-by: Meredith Lancaster <malancas@github.com>

* typo

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use the iostreams.Test helper func

Signed-off-by: Meredith Lancaster <malancas@github.com>

* create interface for oci client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add tests for oci client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename files

Signed-off-by: Meredith Lancaster <malancas@github.com>

* format file

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix shellcheck issues

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use testing TempDir method

Signed-off-by: Meredith Lancaster <malancas@github.com>

* cleanup unused tempdir handling

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use table driven tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* check correct cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* support repo option in download sub cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* switch over to using RunE

Signed-off-by: Meredith Lancaster <malancas@github.com>

* unexport top level subcommand funcs

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add comment around keychain option

Signed-off-by: Meredith Lancaster <malancas@github.com>

* update comments

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix inconsistent naming

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add tests for CLI commands

Signed-off-by: Meredith Lancaster <malancas@github.com>

* check for noattestationsfound err

Signed-off-by: Meredith Lancaster <malancas@github.com>

* try out metadata abstraction instead

Signed-off-by: Meredith Lancaster <malancas@github.com>

* switch to using MetadataStore abstraction

Signed-off-by: Meredith Lancaster <malancas@github.com>

* include test case with failing metadata store

Signed-off-by: Meredith Lancaster <malancas@github.com>

* look for err specific to file write

Signed-off-by: Meredith Lancaster <malancas@github.com>

* unexport fields

Signed-off-by: Meredith Lancaster <malancas@github.com>

* return err when an unsupported hash alg is provided

Signed-off-by: Meredith Lancaster <malancas@github.com>

* PrintTableToStdOut returns err when rendering fails

Signed-off-by: Meredith Lancaster <malancas@github.com>

* start adding sigstore verifier unit tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add more sigstore verifier specific tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use cli table printer

Signed-off-by: Meredith Lancaster <malancas@github.com>

* return JSON results in slice instead of table

Signed-off-by: Meredith Lancaster <malancas@github.com>

* move mock client to test file

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove unneeded table printer method

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add initial tests for tufrootverify cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* formatting

Signed-off-by: Meredith Lancaster <malancas@github.com>

* cleanup method

Signed-off-by: Meredith Lancaster <malancas@github.com>

* close file in error handling branch

Signed-off-by: Meredith Lancaster <malancas@github.com>

* normalize artifact path

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove unneeded embedded file system

Signed-off-by: Meredith Lancaster <malancas@github.com>

* include image name reference err

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use GH_DEBUG value for io handling

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove quiet and verbose flags

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add more tufrootveriify tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* GitHubTUFOptions no longer needs to return error

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove unneeded slice

Signed-off-by: Meredith Lancaster <malancas@github.com>

* normalize all relative paths

Signed-off-by: Meredith Lancaster <malancas@github.com>

* clean up nil client checks

Signed-off-by: Meredith Lancaster <malancas@github.com>

* set api server based on host

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add comment about http client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use format flag to handle json output in verify cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use format flag to handle json output

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use normalized path for cli test arg

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add tests for json output

Signed-off-by: Meredith Lancaster <malancas@github.com>

* cleanup error wrapping

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use test fixtures correctly by normalizing path

Signed-off-by: Meredith Lancaster <malancas@github.com>

* dont clean

Signed-off-by: Meredith Lancaster <malancas@github.com>

* escape backwards slash for windows files with replace

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use strings.Split func

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use strings.Replace for all command tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use CLI cache dir to store tuf metadata

Signed-off-by: Meredith Lancaster <malancas@github.com>

* Tweaked docstrings for gh attestation download

* Tweaked docstrings for gh attestation verify

* Fix for bug in gh attestation where the wrong hostname was being passed to the API client.

* lets hide tuf-root-verify eh?

* Forgot verify's short str.

* add remote verification test

Signed-off-by: Meredith Lancaster <malancas@github.com>

* Revert "add remote verification test"

This reverts commit c0ceb99ca8.

* update json result handling

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add json tags to struct returned by command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix how json results are handled

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add test to ensure JSON output is valid

Signed-off-by: Meredith Lancaster <malancas@github.com>

---------

Signed-off-by: Meredith Lancaster <malancas@github.com>
Co-authored-by: Phill MV <phillmv@github.com>
2024-04-01 11:13:47 -06:00

208 lines
6.5 KiB
Go

package verification
import (
"fmt"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"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."
)
// AttestationProcessingResult captures processing a given attestation's signature verification and policy evaluation
type AttestationProcessingResult struct {
Attestation *api.Attestation `json:"attestation"`
VerificationResult *verify.VerificationResult `json:"verificationResult"`
}
type SigstoreResults struct {
VerifyResults []*AttestationProcessingResult
Error error
}
type SigstoreConfig struct {
CustomTrustedRoot string
Logger *io.Handler
NoPublicGood bool
}
type SigstoreVerifier struct {
ghVerifier *verify.SignedEntityVerifier
publicGoodVerifier *verify.SignedEntityVerifier
customVerifier *verify.SignedEntityVerifier
policy verify.PolicyBuilder
onlyVerifyWithGithub bool
Logger *io.Handler
}
// 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: %v", err)
}
publicGoodVerifier, err := newPublicGoodVerifier()
if err != nil {
return nil, fmt.Errorf("failed to create Public Good Sigstore verifier: %v", err)
}
ghVerifier, err := newGitHubVerifier()
if err != nil {
return nil, fmt.Errorf("failed to create GitHub Sigstore verifier: %v", 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: %v", 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 []*api.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
}
totalAttestations := len(attestations)
for i, apr := range results {
v.Logger.VerbosePrintf("Verifying attestation %d/%d against the configured Sigstore trust roots\n", i+1, totalAttestations)
// 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: %v", 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\": %v", 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: %v", trustedRootFilePath, err)
}
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1))
if err != nil {
return nil, fmt.Errorf("failed to create custom verifier: %v", err)
}
return gv, nil
}
func newGitHubVerifier() (*verify.SignedEntityVerifier, error) {
opts := GitHubTUFOptions()
client, err := tuf.New(opts)
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %v", 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: %v", 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: %v", err)
}
trustedRoot, err := root.GetTrustedRoot(client)
if err != nil {
return nil, fmt.Errorf("failed to get trusted root: %v", 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: %v", err)
}
return sv, nil
}