From 6aeb919d30aeec13c453e4bce6cc6d10f66f9d45 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Fri, 22 Nov 2024 10:25:06 -0800 Subject: [PATCH] Add e2e tests using a TUF mirror (#3938) * Add e2e tests using a TUF mirror Add a test that, instead of overriding trusted keys with environment variables, actually initializes a TUF cache using a local TUF mirror. This will prepare us to safely make changes to the TUF client. Metadata for the public good instance goes through a different code path, since those keys are embedded in the code. We can't test signing and verifying with the public good instance hermetically, so we rely on conformance tests to catch issues with that path. Signed-off-by: Colleen Murphy * go mod tidy Signed-off-by: Colleen Murphy --------- Signed-off-by: Colleen Murphy --- go.mod | 2 +- test/e2e_test.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++ test/helpers.go | 37 +++++++++++-- 3 files changed, 167 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 3682c2f1760..68883bb3be7 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/spiffe/go-spiffe/v2 v2.3.0 github.com/stretchr/testify v1.9.0 + github.com/theupdateframework/go-tuf/v2 v2.0.1 github.com/transparency-dev/merkle v0.0.2 github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 github.com/xanzy/go-gitlab v0.109.0 @@ -241,7 +242,6 @@ require ( github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/thales-e-security/pool v0.0.2 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect - github.com/theupdateframework/go-tuf/v2 v2.0.1 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/urfave/negroni v1.0.0 // indirect diff --git a/test/e2e_test.go b/test/e2e_test.go index 598d7faa5c0..cd180881424 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -21,6 +21,7 @@ import ( "bytes" "context" "crypto" + "crypto/ed25519" "crypto/rand" "crypto/sha256" "crypto/x509" @@ -35,12 +36,15 @@ import ( "os" "path" "path/filepath" + "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/theupdateframework/go-tuf/v2/metadata" + "github.com/theupdateframework/go-tuf/v2/metadata/repository" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8s "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" @@ -54,6 +58,7 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/dockerfile" "github.com/sigstore/cosign/v2/cmd/cosign/cli/download" "github.com/sigstore/cosign/v2/cmd/cosign/cli/generate" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/initialize" "github.com/sigstore/cosign/v2/cmd/cosign/cli/manifest" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/publickey" @@ -69,6 +74,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/payload" tsaclient "github.com/sigstore/timestamp-authority/pkg/client" "github.com/sigstore/timestamp-authority/pkg/server" @@ -276,6 +282,134 @@ func TestImportSignVerifyClean(t *testing.T) { mustErr(verify(pubKeyPath, imgName, true, nil, "", false), t) } +func downloadTargets(td string, targetsMeta *metadata.Metadata[metadata.TargetsType], t *testing.T) { + home, err := os.UserHomeDir() // fulcio repo was downloaded to $HOME in e2e_test.sh + must(err, t) + targets := map[string]string{ + "rekor.pub": rekorURL + "/api/v1/log/publicKey", + "fulcio.crt.pem": fulcioURL + "/api/v1/rootCert", + "ctfe.pub": filepath.Join(home, "fulcio", "config", "ctfe", "pubkey.pem"), + } + targetsDir := filepath.Join(td, "targets") + must(os.Mkdir(targetsDir, 0700), t) + for name, path := range targets { + targetLocalPath := filepath.Join(targetsDir, name) + if strings.HasPrefix(path, "http") { + fp, err := os.Create(targetLocalPath) + must(err, t) + defer fp.Close() + must(downloadFile(path, fp), t) + } + if strings.HasPrefix(path, "/") { + must(copyFile(path, targetLocalPath), t) + } + targetFileInfo, err := metadata.TargetFile().FromFile(targetLocalPath, "sha256") + must(err, t) + targetsMeta.Signed.Targets[name] = targetFileInfo + } +} + +func setUpTUF(td string, t *testing.T) { + // source: https://github.com/theupdateframework/go-tuf/blob/v2.0.2/examples/repository/basic_repository.go + roles := repository.New() + expiration := time.Now().AddDate(0, 0, 1).UTC() + targets := metadata.Targets(expiration) + roles.SetTargets("targets", targets) + downloadTargets(td, roles.Targets("targets"), t) + snapshot := metadata.Snapshot(expiration) + roles.SetSnapshot(snapshot) + timestamp := metadata.Timestamp(expiration) + roles.SetTimestamp(timestamp) + root := metadata.Root(expiration) + root.Signed.ConsistentSnapshot = false + roles.SetRoot(root) + + _, private, err := ed25519.GenerateKey(nil) + must(err, t) + public, err := metadata.KeyFromPublicKey(private.Public()) + must(err, t) + for _, name := range []string{"targets", "snapshot", "timestamp", "root"} { + err = roles.Root().Signed.AddKey(public, name) + must(err, t) + signer, err := signature.LoadSigner(private, crypto.Hash(0)) + must(err, t) + switch name { + case "targets": + _, err = roles.Targets("targets").Sign(signer) + case "snapshot": + _, err = roles.Snapshot().Sign(signer) + case "timestamp": + _, err = roles.Timestamp().Sign(signer) + case "root": + _, err = roles.Root().Sign(signer) + } + must(err, t) + } + must(roles.Targets("targets").ToFile(filepath.Join(td, "targets.json"), false), t) + must(roles.Snapshot().ToFile(filepath.Join(td, "snapshot.json"), false), t) + must(roles.Timestamp().ToFile(filepath.Join(td, "timestamp.json"), false), t) + must(roles.Root().ToFile(filepath.Join(td, fmt.Sprintf("%d.%s.json", roles.Root().Signed.Version, "root")), false), t) + + must(roles.Root().VerifyDelegate("root", roles.Root()), t) + must(roles.Root().VerifyDelegate("targets", roles.Targets("targets")), t) + must(roles.Root().VerifyDelegate("snapshot", roles.Snapshot()), t) + must(roles.Root().VerifyDelegate("timestamp", roles.Timestamp()), t) +} + +func TestSignVerifyWithTUFMirror(t *testing.T) { + ctx := context.Background() + tufLocalCache := t.TempDir() + t.Setenv("TUF_ROOT", tufLocalCache) + tufMirror := t.TempDir() + setUpTUF(tufMirror, t) + rootPath := filepath.Join(tufMirror, "1.root.json") + tufServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.FileServer(http.Dir(tufMirror)).ServeHTTP(w, r) + })) + mirror := tufServer.URL + must(initialize.DoInitialize(ctx, rootPath, mirror), t) + + // unset the roots that were generated for timestamp signing, they won't work here + err := fulcioroots.ReInit() + if err != nil { + t.Fatal(err) + } + + identityToken, err := getOIDCToken() + if err != nil { + t.Fatal(err) + } + + repo, stop := reg(t) + defer stop() + imgName := path.Join(repo, "cosign-e2e-tuf") + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + ko := options.KeyOpts{ + FulcioURL: fulcioURL, + RekorURL: rekorURL, + IDToken: identityToken, + SkipConfirmation: true, + } + so := options.SignOptions{ + Upload: true, + TlogUpload: true, + SkipConfirmation: true, + } + must(sign.SignCmd(ro, ko, so, []string{imgName}), t) + issuer := os.Getenv("OIDC_URL") + verifyCmd := cliverify.VerifyCommand{ + CertVerifyOptions: options.CertVerifyOptions{ + CertOidcIssuer: issuer, + CertIdentity: certID, + }, + Offline: true, + CheckClaims: true, + } + must(verifyCmd.Exec(ctx, []string{imgName}), t) +} + func TestAttestVerify(t *testing.T) { attestVerify(t, "slsaprovenance", diff --git a/test/helpers.go b/test/helpers.go index 2db7092674f..a53f8c15025 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -519,21 +519,48 @@ func setLocalEnv(t *testing.T, dir string) error { return nil } -// downloadAndSetEnv fetches a URL and sets the given environment variable to point to the downloaded file path. -func downloadAndSetEnv(t *testing.T, url, envVar, dir string) error { +// copyFile copies a file from a source to a destination. +func copyFile(src, dst string) error { + f, err := os.Open(src) + if err != nil { + return fmt.Errorf("error opening source file: %w", err) + } + defer f.Close() + cp, err := os.Create(dst) + if err != nil { + return fmt.Errorf("error creating destination file: %w", err) + } + defer cp.Close() + _, err = io.Copy(cp, f) + if err != nil { + return fmt.Errorf("error copying file: %w", err) + } + return nil +} + +// downloadFile fetches a URL and stores it at the given file path. +func downloadFile(url string, fp *os.File) error { resp, err := http.Get(url) //nolint: gosec if err != nil { return fmt.Errorf("error downloading file: %w", err) } defer resp.Body.Close() + _, err = io.Copy(fp, resp.Body) + if err != nil { + return fmt.Errorf("error writing to file: %w", err) + } + return nil +} + +// downloadAndSetEnv fetches a URL and sets the given environment variable to point to the downloaded file path. +func downloadAndSetEnv(t *testing.T, url, envVar, dir string) error { f, err := os.CreateTemp(dir, "") if err != nil { return fmt.Errorf("error creating temp file: %w", err) } - defer f.Close() - _, err = io.Copy(f, resp.Body) + err = downloadFile(url, f) if err != nil { - return fmt.Errorf("error writing to file: %w", err) + return fmt.Errorf("error downloading file: %w", err) } t.Setenv(envVar, f.Name()) return nil