From fa3076fd8c6ed2bd9c3fb315d07a28cc7c884c02 Mon Sep 17 00:00:00 2001 From: Nell Shamrell Date: Wed, 5 Aug 2026 21:59:20 +0000 Subject: [PATCH] Generate TUF test fixtures at test time instead of committing them TUF metadata expires, so committing pre-built TUF repositories to the tree means the test suite breaks purely with the passage of time. Scaffolding hardcodes a six month expiry with no knob to change it, so regenerating the fixtures only ever buys another six months: these fixtures were already regenerated once (#1999) and, unaddressed, would have expired again and reddened CI in January 2027. Add internal/tuftest, which signs a fresh TUF repository on every call with an expiry relative to now, and use it in pkg/tuf, pkg/apis/policy/v1alpha1, and pkg/reconciler/trustroot in place of the inline base64 blobs and committed tarballs those packages used before. The reconciler's golden files are unaffected because they are derived from the committed keys and certificates rather than from the TUF metadata wrapping them, so only the TUF tarballs and root.json files go away there. The keys and certificates stay committed: they are valid for ten years and keeping them makes the expected reconciler output stable. hack/gentestdata now emits only those, and the TUF generation it used to do is removed. The helper deliberately does not import policy-controller/pkg/tuf, since pkg/tuf's tests are in-package (`package tuf`) and that would form an import cycle; it uses go-tuf directly and scaffolding's CompressFS. It also roots each repository at t.TempDir() rather than the single fixed /tmp/tuf path the old local helper in pkg/tuf used, which could not be called twice in one process. pkg/tuf's TestCompressUncompressFS takes a directory from the helper rather than a prebuilt tarball, so it keeps exercising this package's own CompressFS rather than scaffolding's. TestClientFromSerializedMirrorExpired signs a short-lived repository and waits for it to lapse, so that making fixtures permanently fresh cannot silently mask a loss of expiry validation; it waits on the expiry recorded in the generated metadata rather than the one requested, since go-tuf rounds to the nearest second. Finally, add a guardrail test that scans the tree for TUF metadata in any of the three shapes these fixtures have taken historically - metadata files, tarred repositories, and base64 blobs inlined in Go source - so an expiring fixture cannot quietly reappear. It lists files via `git ls-files` so local build output cannot fail it, and has its own tests covering all three shapes so it cannot decay into always passing. Fixes #2001 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cf89d13a-ea80-4605-a098-9bfdf536903d Signed-off-by: Nell Shamrell --- hack/gentestdata/gentestdata.go | 54 ---- internal/tuftest/guard_test.go | 282 ++++++++++++++++++ internal/tuftest/tuftest.go | 235 +++++++++++++++ internal/tuftest/tuftest_test.go | 161 ++++++++++ .../v1alpha1/trustroot_validation_test.go | 36 +-- pkg/reconciler/trustroot/testdata/root.json | 87 ------ .../rootWithCustomTrustedRootJSON.json | 87 ------ .../testdata/rootWithTrustedRootJSON.json | 87 ------ pkg/reconciler/trustroot/testdata/tufRepo.tar | Bin 3021 -> 0 bytes .../tufRepoWithCustomTrustedRootJSON.tar | Bin 3528 -> 0 bytes .../testdata/tufRepoWithTrustedRootJSON.tar | Bin 3524 -> 0 bytes pkg/reconciler/trustroot/trustroot_test.go | 61 ++-- pkg/tuf/repo_test.go | 181 ++++------- 13 files changed, 789 insertions(+), 482 deletions(-) create mode 100644 internal/tuftest/guard_test.go create mode 100644 internal/tuftest/tuftest.go create mode 100644 internal/tuftest/tuftest_test.go delete mode 100644 pkg/reconciler/trustroot/testdata/root.json delete mode 100644 pkg/reconciler/trustroot/testdata/rootWithCustomTrustedRootJSON.json delete mode 100644 pkg/reconciler/trustroot/testdata/rootWithTrustedRootJSON.json delete mode 100644 pkg/reconciler/trustroot/testdata/tufRepo.tar delete mode 100644 pkg/reconciler/trustroot/testdata/tufRepoWithCustomTrustedRootJSON.tar delete mode 100644 pkg/reconciler/trustroot/testdata/tufRepoWithTrustedRootJSON.tar diff --git a/hack/gentestdata/gentestdata.go b/hack/gentestdata/gentestdata.go index 1797e3aea..c57a406d3 100644 --- a/hack/gentestdata/gentestdata.go +++ b/hack/gentestdata/gentestdata.go @@ -27,7 +27,6 @@ import ( "log" "math/big" "os" - "path" "path/filepath" "time" @@ -35,7 +34,6 @@ import ( "github.com/sigstore/policy-controller/pkg/apis/config" testing "github.com/sigstore/policy-controller/pkg/reconciler/testing/v1alpha1" pbcommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" - "github.com/sigstore/scaffolding/pkg/repo" "github.com/sigstore/sigstore/pkg/cryptoutils" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/timestamppb" @@ -78,29 +76,6 @@ func main() { log.Fatal(err) } - tufRepo, rootJSON, err := genTUFRepo(map[string][]byte{ - "rekor.pem": []byte(sigstoreKeysMap["rekor"]), - "ctfe.pem": []byte(sigstoreKeysMap["ctfe"]), - "fulcio.pem": []byte(sigstoreKeysMap["fulcio"]), - }) - if err != nil { - log.Fatal(err) - } - - tufRepoWithTrustedRootJSON, rootJSONWithTrustedRootJSON, err := genTUFRepo(map[string][]byte{ - "trusted_root.json": marshalledEntry, - }) - if err != nil { - log.Fatal(err) - } - - tufRepoWithCustomTrustedRootJSON, rootJSONWithCustomTrustedRootJSON, err := genTUFRepo(map[string][]byte{ - "custom_trusted_root.json": marshalledEntry, - }) - if err != nil { - log.Fatal(err) - } - marshalledEntryFromMirrorFS, err := genTrustedRoot(sigstoreKeysMap) if err != nil { log.Fatal(err) @@ -114,12 +89,6 @@ func main() { mustWriteFile("tsaCertChain.pem", tsaChainConcat) mustWriteFile("marshalledEntry.json", marshalledEntry) mustWriteFile("marshalledEntryFromMirrorFS.json", marshalledEntryFromMirrorFS) - mustWriteFile("tufRepo.tar", tufRepo) - mustWriteFile("root.json", rootJSON) - mustWriteFile("tufRepoWithTrustedRootJSON.tar", tufRepoWithTrustedRootJSON) - mustWriteFile("rootWithTrustedRootJSON.json", rootJSONWithTrustedRootJSON) - mustWriteFile("tufRepoWithCustomTrustedRootJSON.tar", tufRepoWithCustomTrustedRootJSON) - mustWriteFile("rootWithCustomTrustedRootJSON.json", rootJSONWithCustomTrustedRootJSON) } func mustWriteFile(path string, data []byte) { @@ -235,29 +204,6 @@ func genLogID(pkBytes []byte) (string, error) { return cosign.GetTransparencyLogID(pk) } -func genTUFRepo(files map[string][]byte) ([]byte, []byte, error) { - defer os.RemoveAll(path.Join(os.TempDir(), "tuf")) // TODO: Update scaffolding to use os.MkdirTemp and remove this - ctx := context.Background() - local, dir, err := repo.CreateRepoWithOptions(ctx, files, repo.CreateRepoOptions{AddMetadataTargets: true}) - if err != nil { - return nil, nil, err - } - meta, err := local.GetMeta() - if err != nil { - return nil, nil, err - } - rootJSON, ok := meta["root.json"] - if !ok { - return nil, nil, err - } - - var compressed bytes.Buffer - if err := repo.CompressFS(os.DirFS(dir), &compressed, map[string]bool{"keys": true, "staged": true}); err != nil { - return nil, nil, err - } - return compressed.Bytes(), rootJSON, nil -} - func genTrustedRoot(sigstoreKeysMap map[string]string) ([]byte, error) { tlogKey, _, err := config.DeserializePublicKey([]byte(sigstoreKeysMap["rekor"])) if err != nil { diff --git a/internal/tuftest/guard_test.go b/internal/tuftest/guard_test.go new file mode 100644 index 000000000..5d7bee124 --- /dev/null +++ b/internal/tuftest/guard_test.go @@ -0,0 +1,282 @@ +// Copyright 2026 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tuftest + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "errors" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "testing" +) + +// skipDirs are directories that are not ours to police. +var skipDirs = map[string]bool{ + ".git": true, + "vendor": true, + "third_party": true, + "node_modules": true, +} + +const guidance = `Committed TUF metadata expires, which breaks CI purely with the passage of +time (see https://github.com/sigstore/policy-controller/issues/2001). Generate +repositories at test time with internal/tuftest instead of committing them.` + +// TestNoCommittedTUFMetadata fails if TUF metadata is committed anywhere in the +// tree. This is a guardrail rather than a test of behaviour: it exists so that +// a pre-built, and therefore expiring, TUF repository cannot quietly reappear. +// +// It covers the three shapes these fixtures have historically taken: metadata +// files, tarred repositories, and base64-encoded repositories inlined into Go +// source. +func TestNoCommittedTUFMetadata(t *testing.T) { + root := repoRoot(t) + + var found []string + for _, rel := range committedFiles(t, root) { + path := filepath.Join(root, rel) + if reason := inspect(path); reason != "" { + found = append(found, rel+" ("+reason+")") + } + } + + if len(found) > 0 { + t.Errorf("found committed TUF metadata:\n\t%s\n\n%s", strings.Join(found, "\n\t"), guidance) + } +} + +// inspect returns a description of the TUF metadata in path, or the empty +// string if there is none. +func inspect(path string) string { + switch { + case strings.HasSuffix(path, ".json"): + if expires := tufExpiry(mustRead(path)); expires != "" { + return "TUF metadata expiring " + expires + } + case strings.HasSuffix(path, ".tar"), strings.HasSuffix(path, ".tar.gz"), strings.HasSuffix(path, ".tgz"): + if archiveHasTUFMetadata(mustRead(path)) { + return "archive containing a TUF repository" + } + case strings.HasSuffix(path, ".go"): + if reason := inlinedTUFMetadata(mustRead(path)); reason != "" { + return reason + } + } + return "" +} + +func mustRead(path string) []byte { + b, err := os.ReadFile(path) + if err != nil { + return nil + } + return b +} + +// base64Literal matches the long base64 string literals these fixtures were +// historically stored in. The lower bound keeps ordinary +// constants out; real TUF metadata encodes to several KB. +var base64Literal = regexp.MustCompile("[`\"]([A-Za-z0-9+/=]{200,})[`\"]") + +// inlinedTUFMetadata looks for base64-encoded TUF metadata or repositories +// embedded in Go source, which is the form the fixtures in pkg/tuf and +// pkg/apis/policy/v1alpha1 used to take. +func inlinedTUFMetadata(src []byte) string { + for _, m := range base64Literal.FindAllSubmatch(src, -1) { + decoded, err := base64.StdEncoding.DecodeString(string(m[1])) + if err != nil { + continue + } + if expires := tufExpiry(decoded); expires != "" { + return "base64-encoded TUF metadata expiring " + expires + } + if archiveHasTUFMetadata(decoded) { + return "base64-encoded archive containing a TUF repository" + } + } + return "" +} + +// tufExpiry returns the signed.expires value if b looks like TUF metadata, and +// the empty string otherwise. Anything that is not JSON is simply not TUF +// metadata for our purposes. +func tufExpiry(b []byte) string { + var doc struct { + Signed struct { + Type string `json:"_type"` + Expires string `json:"expires"` + } `json:"signed"` + } + if err := json.Unmarshal(b, &doc); err != nil { + return "" + } + if doc.Signed.Type == "" { + return "" + } + return doc.Signed.Expires +} + +// archiveHasTUFMetadata reports whether a tar archive contains a root.json that +// really is TUF metadata. Matching on the filename alone would reject unrelated +// archives that happen to carry a file of that name. +func archiveHasTUFMetadata(b []byte) bool { + var r io.Reader = bytes.NewReader(b) + if len(b) > 2 && b[0] == 0x1f && b[1] == 0x8b { + zr, err := gzip.NewReader(bytes.NewReader(b)) + if err != nil { + return false + } + defer zr.Close() + r = zr + } + + tr := tar.NewReader(r) + for { + hdr, err := tr.Next() + if errors.Is(err, io.EOF) { + return false + } + if err != nil { + return false + } + if filepath.Base(hdr.Name) != "root.json" { + continue + } + content, err := io.ReadAll(io.LimitReader(tr, 1<<20)) + if err != nil { + return false + } + if tufExpiry(content) != "" { + return true + } + } +} + +// committedFiles lists the files tracked by git, so that untracked local +// artifacts such as build output cannot fail the guardrail. It falls back to +// walking the tree where git is unavailable, e.g. in a source archive. +func committedFiles(t *testing.T, root string) []string { + t.Helper() + + out, err := exec.Command("git", "-C", root, "ls-files", "-z").Output() + if err == nil { + var files []string + for _, name := range strings.Split(string(out), "\x00") { + if name != "" { + files = append(files, name) + } + } + return files + } + + t.Logf("git ls-files unavailable (%v), falling back to walking %s", err, root) + var files []string + if err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + if skipDirs[d.Name()] { + return fs.SkipDir + } + return nil + } + rel, relErr := filepath.Rel(root, path) + if relErr != nil { + return relErr + } + files = append(files, rel) + return nil + }); err != nil { + t.Fatalf("walking %s: %v", root, err) + } + return files +} + +// repoRoot walks up from this source file to the directory holding go.mod. +func repoRoot(t *testing.T) string { + t.Helper() + + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("could not determine the path of this source file") + } + dir := filepath.Dir(file) + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + t.Fatal("could not find go.mod above " + file) + } + dir = parent + } +} + +// TestGuardDetectsFixtureShapes covers the three shapes the guardrail is meant +// to catch, so that it cannot silently degrade into always passing. +func TestGuardDetectsFixtureShapes(t *testing.T) { + tarGz, rootJSON := NewRepo(t, testTargets()) + + t.Run("metadata file", func(t *testing.T) { + if got := tufExpiry(rootJSON); got == "" { + t.Error("did not recognise a root.json as TUF metadata") + } + }) + + t.Run("tarred repository", func(t *testing.T) { + if !archiveHasTUFMetadata(tarGz) { + t.Error("did not recognise a tarred TUF repository") + } + }) + + t.Run("base64 in Go source", func(t *testing.T) { + src := []byte("package x\n\nconst validRepository = `" + + base64.StdEncoding.EncodeToString(tarGz) + "`\n") + if got := inlinedTUFMetadata(src); got == "" { + t.Error("did not recognise a base64-encoded repository inlined in Go source") + } + + src = []byte("package x\n\nconst rootJSON = `" + + base64.StdEncoding.EncodeToString(rootJSON) + "`\n") + if got := inlinedTUFMetadata(src); got == "" { + t.Error("did not recognise base64-encoded TUF metadata inlined in Go source") + } + }) + + t.Run("ignores unrelated content", func(t *testing.T) { + if got := tufExpiry([]byte(`{"hello":"world"}`)); got != "" { + t.Errorf("plain JSON reported as TUF metadata: %s", got) + } + if archiveHasTUFMetadata([]byte("not an archive")) { + t.Error("non-archive reported as a TUF repository") + } + if got := inlinedTUFMetadata([]byte("package x\n\nconst k = `" + + strings.Repeat("A", 600) + "`\n")); got != "" { + t.Errorf("unrelated base64 literal reported as TUF metadata: %s", got) + } + }) +} diff --git a/internal/tuftest/tuftest.go b/internal/tuftest/tuftest.go new file mode 100644 index 000000000..a1734ae0c --- /dev/null +++ b/internal/tuftest/tuftest.go @@ -0,0 +1,235 @@ +// Copyright 2026 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tuftest builds throwaway TUF repositories for use in tests. +// +// TUF metadata expires, so committing pre-built repositories to the tree means +// the test suite breaks purely with the passage of time. Tests should call +// NewRepo or NewRepoDir instead, which sign fresh metadata on every run with an +// expiry relative to "now". +// +// This package deliberately does not import +// github.com/sigstore/policy-controller/pkg/tuf: pkg/tuf's own tests are +// in-package (`package tuf`), so such an import would form an import cycle. +package tuftest + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/sigstore/scaffolding/pkg/repo" + "github.com/theupdateframework/go-tuf" +) + +// DefaultValidity is how far in the future generated metadata expires. It +// matches the six months used by sigstore/scaffolding, but because it is always +// applied relative to time.Now() the resulting repository never goes stale. +const DefaultValidity = 6 * 30 * 24 * time.Hour + +// roles are the top-level TUF roles that get a generated signing key. +var roles = []string{"root", "targets", "snapshot", "timestamp"} + +// Target is a file to publish in the generated repository. +type Target struct { + // Name is the target path within the repository, e.g. "rekor.pem". The + // name is significant: sigstore derives a target's usage from it, so + // callers should keep using the same filenames as production repositories. + Name string + + // Bytes is the target's content. + Bytes []byte + + // CustomMetadata is the target's TUF custom metadata. When nil, sigstore + // custom metadata is generated from Name, matching what + // scaffolding's repo.CreateRepoWithOptions would have produced. + CustomMetadata []byte +} + +type config struct { + expires time.Time + consistentSnapshot bool + addCustomMetadata bool +} + +// Option configures repository generation. +type Option func(*config) + +// WithExpires sets the expiry stamped into every generated metadata file. It +// must be in the future: go-tuf refuses to sign metadata that has already +// expired. To exercise expiry handling, set a short validity and wait for it to +// lapse, reading the effective expiry back out of the generated metadata, since +// go-tuf rounds to the nearest second. +func WithExpires(t time.Time) Option { + return func(c *config) { c.expires = t } +} + +// WithConsistentSnapshot controls the TUF consistent snapshot setting. It +// defaults to true, matching sigstore/scaffolding and therefore production +// sigstore TUF repositories. +func WithConsistentSnapshot(b bool) Option { + return func(c *config) { c.consistentSnapshot = b } +} + +// WithSigstoreCustomMetadata controls whether targets without explicit +// CustomMetadata get sigstore usage metadata generated from their name. It +// defaults to true. +func WithSigstoreCustomMetadata(b bool) Option { + return func(c *config) { c.addCustomMetadata = b } +} + +// customMetadata mirrors the shape scaffolding writes for each target. +type customMetadata struct { + Sigstore struct { + Usage string `json:"usage"` + Status string `json:"status"` + URI string `json:"uri"` + } `json:"sigstore"` +} + +// targetUsage reproduces scaffolding's filename-based usage derivation. +func targetUsage(name string) string { + for _, known := range []string{repo.FulcioTarget, repo.RekorTarget, repo.CTFETarget, repo.TSATarget} { + if strings.Contains(strings.ToLower(name), strings.ToLower(known)) { + return known + } + } + return repo.UnknownTarget +} + +func sigstoreMetadata(name string) ([]byte, error) { + var cm customMetadata + cm.Sigstore.Usage = targetUsage(name) + cm.Sigstore.Status = "Active" + return json.Marshal(&cm) +} + +// NewRepoDir creates and commits a TUF repository containing targets in a +// temporary directory scoped to t, and returns the backing store along with the +// repository's root directory. The directory contains the usual "repository", +// "staged" and "keys" subdirectories. +// +// Unlike scaffolding's repo.CreateRepoWithMetadata, which always writes to a +// single fixed path under os.TempDir, this is safe to call repeatedly and from +// tests running in parallel. +func NewRepoDir(t *testing.T, targets []Target, opts ...Option) (tuf.LocalStore, string) { + t.Helper() + + cfg := config{ + expires: time.Now().Add(DefaultValidity), + consistentSnapshot: true, + addCustomMetadata: true, + } + for _, opt := range opts { + opt(&cfg) + } + + dir := t.TempDir() + local := tuf.FileSystemStore(dir, nil) + + r, err := tuf.NewRepoIndent(local, "", " ") + if err != nil { + t.Fatalf("tuftest: NewRepoIndent: %v", err) + } + if err := r.Init(cfg.consistentSnapshot); err != nil { + t.Fatalf("tuftest: Init: %v", err) + } + + for _, role := range roles { + if _, err := r.GenKeyWithExpires(role, cfg.expires); err != nil { + t.Fatalf("tuftest: GenKeyWithExpires(%s): %v", role, err) + } + } + + for _, target := range targets { + if err := writeStagedTarget(dir, target.Name, target.Bytes); err != nil { + t.Fatalf("tuftest: staging target %s: %v", target.Name, err) + } + meta := target.CustomMetadata + if meta == nil && cfg.addCustomMetadata { + if meta, err = sigstoreMetadata(target.Name); err != nil { + t.Fatalf("tuftest: custom metadata for %s: %v", target.Name, err) + } + } + if err := r.AddTargetWithExpires(target.Name, meta, cfg.expires); err != nil { + t.Fatalf("tuftest: AddTargetWithExpires(%s): %v", target.Name, err) + } + } + + if err := r.SnapshotWithExpires(cfg.expires); err != nil { + t.Fatalf("tuftest: SnapshotWithExpires: %v", err) + } + if err := r.TimestampWithExpires(cfg.expires); err != nil { + t.Fatalf("tuftest: TimestampWithExpires: %v", err) + } + if err := r.Commit(); err != nil { + t.Fatalf("tuftest: Commit: %v", err) + } + + return local, dir +} + +// NewRepo creates a TUF repository containing targets and returns it serialized +// as a gzipped tarball, along with its root.json. The tarball is rooted at +// "repository/" and excludes private key material, so it is suitable for a +// TrustRoot's mirrorFS. +// +// Callers testing this repository's own compression helpers should use +// NewRepoDir and compress the directory themselves, so that the code under test +// is the code actually exercised. +func NewRepo(t *testing.T, targets []Target, opts ...Option) (tarGz, rootJSON []byte) { + t.Helper() + + local, dir := NewRepoDir(t, targets, opts...) + + meta, err := local.GetMeta() + if err != nil { + t.Fatalf("tuftest: GetMeta: %v", err) + } + rootJSON, ok := meta["root.json"] + if !ok { + t.Fatal("tuftest: generated repository has no root.json") + } + + return Compress(t, dir), rootJSON +} + +// Compress archives a repository directory produced by NewRepoDir, skipping the +// private keys and staging areas. +func Compress(t *testing.T, dir string) []byte { + t.Helper() + + var buf bytes.Buffer + if err := repo.CompressFS(os.DirFS(dir), &buf, map[string]bool{"keys": true, "staged": true}); err != nil { + t.Fatalf("tuftest: CompressFS: %v", err) + } + return buf.Bytes() +} + +func writeStagedTarget(dir, name string, data []byte) error { + path := filepath.Join(dir, "staged", "targets", name) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return fmt.Errorf("creating staged target dir: %w", err) + } + /* #nosec G306 */ + if err := os.WriteFile(path, data, 0o644); err != nil { + return fmt.Errorf("writing staged target: %w", err) + } + return nil +} diff --git a/internal/tuftest/tuftest_test.go b/internal/tuftest/tuftest_test.go new file mode 100644 index 000000000..22cc087a9 --- /dev/null +++ b/internal/tuftest/tuftest_test.go @@ -0,0 +1,161 @@ +// Copyright 2026 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tuftest + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "encoding/json" + "errors" + "io" + "strings" + "testing" + "time" +) + +func testTargets() []Target { + return []Target{ + {Name: "rekor.pem", Bytes: []byte("rekor")}, + {Name: "fulcio.pem", Bytes: []byte("fulcio")}, + } +} + +// TestExpiryIsRelativeToNow is the point of this package: the generated +// metadata must expire relative to the current time, never on a fixed date. +func TestExpiryIsRelativeToNow(t *testing.T) { + _, rootJSON := NewRepo(t, testTargets()) + + var doc struct { + Signed struct { + Expires time.Time `json:"expires"` + } `json:"signed"` + } + if err := json.Unmarshal(rootJSON, &doc); err != nil { + t.Fatalf("unmarshalling root.json: %v", err) + } + + want := time.Now().Add(DefaultValidity) + if diff := doc.Signed.Expires.Sub(want); diff > time.Hour || diff < -time.Hour { + t.Errorf("root.json expires at %s, want approximately %s (off by %s); "+ + "an expiry that is not relative to now will rot", + doc.Signed.Expires, want, diff) + } +} + +func TestWithExpires(t *testing.T) { + want := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second) + _, rootJSON := NewRepo(t, testTargets(), WithExpires(want)) + + var doc struct { + Signed struct { + Expires time.Time `json:"expires"` + } `json:"signed"` + } + if err := json.Unmarshal(rootJSON, &doc); err != nil { + t.Fatalf("unmarshalling root.json: %v", err) + } + if !doc.Signed.Expires.Equal(want) { + t.Errorf("root.json expires at %s, want %s", doc.Signed.Expires, want) + } +} + +// TestArchiveExcludesPrivateKeys makes sure the tarball handed to tests, which +// stands in for a TrustRoot's mirrorFS, never carries signing keys. +func TestArchiveExcludesPrivateKeys(t *testing.T) { + tarGz, _ := NewRepo(t, testTargets()) + + names := archiveNames(t, tarGz) + if len(names) == 0 { + t.Fatal("archive is empty") + } + for _, name := range names { + if strings.HasPrefix(name, "keys/") || strings.HasPrefix(name, "staged/") { + t.Errorf("archive contains %q, which may hold private key material", name) + } + if name != "repository" && !strings.HasPrefix(name, "repository/") { + t.Errorf("archive entry %q is not under repository/", name) + } + } +} + +func TestConsistentSnapshot(t *testing.T) { + for _, consistent := range []bool{true, false} { + t.Run(map[bool]string{true: "enabled", false: "disabled"}[consistent], func(t *testing.T) { + _, rootJSON := NewRepo(t, testTargets(), WithConsistentSnapshot(consistent)) + + var doc struct { + Signed struct { + ConsistentSnapshot bool `json:"consistent_snapshot"` + } `json:"signed"` + } + if err := json.Unmarshal(rootJSON, &doc); err != nil { + t.Fatalf("unmarshalling root.json: %v", err) + } + if doc.Signed.ConsistentSnapshot != consistent { + t.Errorf("consistent_snapshot = %v, want %v", doc.Signed.ConsistentSnapshot, consistent) + } + }) + } +} + +// TestSigstoreCustomMetadata covers the usage values the trustroot reconciler +// reads back out of each target's custom metadata. +func TestSigstoreCustomMetadata(t *testing.T) { + for name, want := range map[string]string{ + "rekor.pem": "Rekor", + "fulcio.pem": "Fulcio", + "ctfe.pem": "CTFE", + "tsa_leaf.pem": "TSA", + "something.pem": "Unknown", + } { + if got := targetUsage(name); got != want { + t.Errorf("targetUsage(%q) = %q, want %q", name, got, want) + } + } +} + +// TestNewRepoDirIsIsolated covers the collision that the old fixed /tmp/tuf +// path caused: generating two repositories in one test must just work. +func TestNewRepoDirIsIsolated(t *testing.T) { + _, first := NewRepoDir(t, testTargets()) + _, second := NewRepoDir(t, testTargets()) + if first == second { + t.Errorf("both repositories were created in %s", first) + } +} + +func archiveNames(t *testing.T, tarGz []byte) []string { + t.Helper() + + zr, err := gzip.NewReader(bytes.NewReader(tarGz)) + if err != nil { + t.Fatalf("opening gzip stream: %v", err) + } + defer zr.Close() + + var names []string + tr := tar.NewReader(zr) + for { + hdr, err := tr.Next() + if errors.Is(err, io.EOF) { + return names + } + if err != nil { + t.Fatalf("reading archive: %v", err) + } + names = append(names, hdr.Name) + } +} diff --git a/pkg/apis/policy/v1alpha1/trustroot_validation_test.go b/pkg/apis/policy/v1alpha1/trustroot_validation_test.go index ce0a58797..e3eddfec2 100644 --- a/pkg/apis/policy/v1alpha1/trustroot_validation_test.go +++ b/pkg/apis/policy/v1alpha1/trustroot_validation_test.go @@ -17,41 +17,33 @@ package v1alpha1 import ( "context" "crypto/x509" - "encoding/base64" "testing" + "github.com/sigstore/policy-controller/internal/tuftest" "github.com/sigstore/policy-controller/test" "github.com/sigstore/sigstore/pkg/cryptoutils" "knative.dev/pkg/apis" ) -// validRepository is a TUF repository that's been tarred, gzipped and base64 -// encoded. These are vars because conversion to []byte seems to make them not -// constant -var ( - validRepository = `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` - - // This is valid base64 (hello world), but should not be able to gunzip - // untar. - invalidRepository = []byte(`aGVsbG8gd29ybGQK`) - - // TUF Root json for the above validRepository. - // IMPORTANT: The next expiration is on '2027-01-28T17:36:23Z' - // To regenerate rootJSON and the matching validRepository above, run - // `make generate-testdata` and base64-encode the resulting - // pkg/reconciler/trustroot/testdata/root.json and tufRepo.tar respectively. - rootJSON = `ewogInNpZ25lZCI6IHsKICAiX3R5cGUiOiAicm9vdCIsCiAgInNwZWNfdmVyc2lvbiI6ICIxLjAiLAogICJ2ZXJzaW9uIjogMSwKICAiZXhwaXJlcyI6ICIyMDI3LTAxLTI4VDE3OjM2OjIzWiIsCiAgImtleXMiOiB7CiAgICIzZDk1NWRlZTk5ODMwYmUwNTc1MWQyMmNjMDUwYTQ3NWI2OTIwZGUyZDM2MWNiZGVhNjJmYzE0YTY2MTg3MzU5IjogewogICAgImtleXR5cGUiOiAiZWQyNTUxOSIsCiAgICAic2NoZW1lIjogImVkMjU1MTkiLAogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICIwOWI2MjM5ZTkyNTBjMWEzODIyY2UwY2YzZmMxMjdiMjY3YmUwZTUyYWZlYjA0YWY4MDQ2MmM0ZmM2MWE4NDI0IgogICAgfQogICB9LAogICAiNTU0NjRhMzRhMzk1NWQ3NTMxMzE0M2Y0ZThmZGE0NzRlNWRkMmYwNjcwYTc5ZGE0ZDIzZDhiMWNkZDQ3ZDNlMiI6IHsKICAgICJrZXl0eXBlIjogImVkMjU1MTkiLAogICAgInNjaGVtZSI6ICJlZDI1NTE5IiwKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiYjIyYzQ3NDM5ZGIxNDk5MDhlY2ZhOTA0MDZhZDA5MzJjMzNkOTkwOThhODQ3MWY1ZmU3MjMwZjRkZjRjZmJkZSIKICAgIH0KICAgfSwKICAgIjZjMDc5MzVkY2RjZDRjNmM4NmUxZGM3OTc1NmY2MTY5MDZkZDhhNWQ1YWRjN2NlN2NlMzg1YTllYTIyMTlmMGEiOiB7CiAgICAia2V5dHlwZSI6ICJlZDI1NTE5IiwKICAgICJzY2hlbWUiOiAiZWQyNTUxOSIsCiAgICAia2V5aWRfaGFzaF9hbGdvcml0aG1zIjogWwogICAgICJzaGEyNTYiLAogICAgICJzaGE1MTIiCiAgICBdLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogImZkNWM1YzhkNmU2MTFkNTdkYWIxYzk0YjEyODVkMmE1NWI3OWQ5MTg3MGY0ZjBhZjg1ZDRiMTkyZmRlYjdkMjUiCiAgICB9CiAgIH0sCiAgICJmMDBlMTU3YTc1OGU3ZGZlMmY3OTc0N2RmNzgwNjM4MmFmNzBhODc0YmIzZTJjZTc2MmJhOThkOWQ4NGU5YWRmIjogewogICAgImtleXR5cGUiOiAiZWQyNTUxOSIsCiAgICAic2NoZW1lIjogImVkMjU1MTkiLAogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICI5MmU0NGFmNTgyNzU4NjQ1ODVmYzVlMjI2NmRjMDdkZWFjNzFiY2RmN2Q3YzVkYWI2NWQ2MTYxY2E1MDAxN2JiIgogICAgfQogICB9CiAgfSwKICAicm9sZXMiOiB7CiAgICJyb290IjogewogICAgImtleWlkcyI6IFsKICAgICAiNTU0NjRhMzRhMzk1NWQ3NTMxMzE0M2Y0ZThmZGE0NzRlNWRkMmYwNjcwYTc5ZGE0ZDIzZDhiMWNkZDQ3ZDNlMiIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMQogICB9LAogICAic25hcHNob3QiOiB7CiAgICAia2V5aWRzIjogWwogICAgICIzZDk1NWRlZTk5ODMwYmUwNTc1MWQyMmNjMDUwYTQ3NWI2OTIwZGUyZDM2MWNiZGVhNjJmYzE0YTY2MTg3MzU5IgogICAgXSwKICAgICJ0aHJlc2hvbGQiOiAxCiAgIH0sCiAgICJ0YXJnZXRzIjogewogICAgImtleWlkcyI6IFsKICAgICAiNmMwNzkzNWRjZGNkNGM2Yzg2ZTFkYzc5NzU2ZjYxNjkwNmRkOGE1ZDVhZGM3Y2U3Y2UzODVhOWVhMjIxOWYwYSIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMQogICB9LAogICAidGltZXN0YW1wIjogewogICAgImtleWlkcyI6IFsKICAgICAiZjAwZTE1N2E3NThlN2RmZTJmNzk3NDdkZjc4MDYzODJhZjcwYTg3NGJiM2UyY2U3NjJiYTk4ZDlkODRlOWFkZiIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMQogICB9CiAgfSwKICAiY29uc2lzdGVudF9zbmFwc2hvdCI6IHRydWUKIH0sCiAic2lnbmF0dXJlcyI6IFsKICB7CiAgICJrZXlpZCI6ICI1NTQ2NGEzNGEzOTU1ZDc1MzEzMTQzZjRlOGZkYTQ3NGU1ZGQyZjA2NzBhNzlkYTRkMjNkOGIxY2RkNDdkM2UyIiwKICAgInNpZyI6ICJjNDg2MDhlYTRjMzY3N2QyNTE5OTQyMWJiYTM5YTE3NTM5YjRmOGUyMzU4MWEzMWQ3YjY2NDIyNTAxOGYxNmRjOTE1OTgyNjM2YjlhMDMxODc2ZjAyYzY5YmQxMTFjZTA4YTg0ZWQzODY5YjI0ZDZhNDg0YTY1YjJmZmE2Y2IwOSIKICB9CiBdCn0=` -) +// invalidRepository is valid base64 (hello world), but should not be able to +// gunzip untar. +var invalidRepository = []byte(`aGVsbG8gd29ybGQK`) func TestTrustRootValidation(t *testing.T) { - rootJSONDecoded, err := base64.StdEncoding.DecodeString(rootJSON) + rootCert, _, err := test.GenerateRootCa() if err != nil { - t.Fatalf("Failed to decode rootJSON for testing: %v", err) + t.Fatalf("Failed to generate a root CA for testing: %v", err) } - validRepositoryDecoded, err := base64.StdEncoding.DecodeString(validRepository) + fulcioPEM, err := cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{rootCert}) if err != nil { - t.Fatalf("Failed to decode validRepository for testing: %v", err) + t.Fatalf("Failed to marshal the root CA for testing: %v", err) } + + // Generate the TUF repository at test time so that its metadata is never + // expired, rather than committing a repository that goes stale. + validRepositoryDecoded, rootJSONDecoded := tuftest.NewRepo(t, []tuftest.Target{ + {Name: "fulcio_v1.crt.pem", Bytes: fulcioPEM}, + }) tests := []struct { name string trustroot TrustRoot diff --git a/pkg/reconciler/trustroot/testdata/root.json b/pkg/reconciler/trustroot/testdata/root.json deleted file mode 100644 index 4926c667b..000000000 --- a/pkg/reconciler/trustroot/testdata/root.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "signed": { - "_type": "root", - "spec_version": "1.0", - "version": 1, - "expires": "2027-01-28T17:36:23Z", - "keys": { - "3d955dee99830be05751d22cc050a475b6920de2d361cbdea62fc14a66187359": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "09b6239e9250c1a3822ce0cf3fc127b267be0e52afeb04af80462c4fc61a8424" - } - }, - "55464a34a3955d75313143f4e8fda474e5dd2f0670a79da4d23d8b1cdd47d3e2": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "b22c47439db149908ecfa90406ad0932c33d99098a8471f5fe7230f4df4cfbde" - } - }, - "6c07935dcdcd4c6c86e1dc79756f616906dd8a5d5adc7ce7ce385a9ea2219f0a": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "fd5c5c8d6e611d57dab1c94b1285d2a55b79d91870f4f0af85d4b192fdeb7d25" - } - }, - "f00e157a758e7dfe2f79747df7806382af70a874bb3e2ce762ba98d9d84e9adf": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "92e44af58275864585fc5e2266dc07deac71bcdf7d7c5dab65d6161ca50017bb" - } - } - }, - "roles": { - "root": { - "keyids": [ - "55464a34a3955d75313143f4e8fda474e5dd2f0670a79da4d23d8b1cdd47d3e2" - ], - "threshold": 1 - }, - "snapshot": { - "keyids": [ - "3d955dee99830be05751d22cc050a475b6920de2d361cbdea62fc14a66187359" - ], - "threshold": 1 - }, - "targets": { - "keyids": [ - "6c07935dcdcd4c6c86e1dc79756f616906dd8a5d5adc7ce7ce385a9ea2219f0a" - ], - "threshold": 1 - }, - "timestamp": { - "keyids": [ - "f00e157a758e7dfe2f79747df7806382af70a874bb3e2ce762ba98d9d84e9adf" - ], - "threshold": 1 - } - }, - "consistent_snapshot": true - }, - "signatures": [ - { - "keyid": "55464a34a3955d75313143f4e8fda474e5dd2f0670a79da4d23d8b1cdd47d3e2", - "sig": "c48608ea4c3677d25199421bba39a17539b4f8e23581a31d7b664225018f16dc915982636b9a031876f02c69bd111ce08a84ed3869b24d6a484a65b2ffa6cb09" - } - ] -} \ No newline at end of file diff --git a/pkg/reconciler/trustroot/testdata/rootWithCustomTrustedRootJSON.json b/pkg/reconciler/trustroot/testdata/rootWithCustomTrustedRootJSON.json deleted file mode 100644 index 9d861c956..000000000 --- a/pkg/reconciler/trustroot/testdata/rootWithCustomTrustedRootJSON.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "signed": { - "_type": "root", - "spec_version": "1.0", - "version": 1, - "expires": "2027-01-28T17:36:23Z", - "keys": { - "a0b9452b80cd34c62f8ca7ec53227c2781aa81b62d599afc92a85253b9b2e3d4": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "1aba272d9f42bf9db9e67c15549722a7d4ca3e9d94efaa9946cb76a0a9ff7bc2" - } - }, - "a2031764f95a4eefcc448920e7f7f585e6e9187e168f5bca9828413c7df736f8": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "83dff562309be43e410cff55e6619614a62048ea056e3eb3078beb08a27a6ad0" - } - }, - "a49b7303a06d9b64abd836e575e1a57698ddd0cbaf6a4bd589d8f752cd820e1a": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "af4e935f1b07bdc4c3ddaf3bba427ba704d83b31ec5f75705f60324bd706a480" - } - }, - "d8d64f3c26f6ee1cec851cdbd4714d5c71cbe87de954a416a87a445c1187d8df": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "d222157c26a20193b6f5770bdef84c1d37e33aa741dc3d83c535bd1856dee592" - } - } - }, - "roles": { - "root": { - "keyids": [ - "a49b7303a06d9b64abd836e575e1a57698ddd0cbaf6a4bd589d8f752cd820e1a" - ], - "threshold": 1 - }, - "snapshot": { - "keyids": [ - "d8d64f3c26f6ee1cec851cdbd4714d5c71cbe87de954a416a87a445c1187d8df" - ], - "threshold": 1 - }, - "targets": { - "keyids": [ - "a2031764f95a4eefcc448920e7f7f585e6e9187e168f5bca9828413c7df736f8" - ], - "threshold": 1 - }, - "timestamp": { - "keyids": [ - "a0b9452b80cd34c62f8ca7ec53227c2781aa81b62d599afc92a85253b9b2e3d4" - ], - "threshold": 1 - } - }, - "consistent_snapshot": true - }, - "signatures": [ - { - "keyid": "a49b7303a06d9b64abd836e575e1a57698ddd0cbaf6a4bd589d8f752cd820e1a", - "sig": "4e484b0e09dc6884959d566781e698ce454ce5aedfc560a890beecf57fcf843a6653aa5c179ad56727bec994e5c25f59f374fcd70e02613f09d1a6eec41af301" - } - ] -} \ No newline at end of file diff --git a/pkg/reconciler/trustroot/testdata/rootWithTrustedRootJSON.json b/pkg/reconciler/trustroot/testdata/rootWithTrustedRootJSON.json deleted file mode 100644 index 6713f981d..000000000 --- a/pkg/reconciler/trustroot/testdata/rootWithTrustedRootJSON.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "signed": { - "_type": "root", - "spec_version": "1.0", - "version": 1, - "expires": "2027-01-28T17:36:23Z", - "keys": { - "68112049b36f72691c2ada4409c273fae84a98cb19abb7e348025d8374b9d293": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "120951fa65543ab7d3f8af0e42052cb5e3a8ec594fc948610cad79b73de9d4c0" - } - }, - "a0986ef13d95e0885948fa0aa9dc6d15efe784e8738baa9007754302fc5223af": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "cdd30d1c75ed83815a2cdd2c6a1c4df1f5dfaf4899ac2257ac6db20a9a151f8d" - } - }, - "aef156f2d58836ca19117799ae800f21447401858b8291ab8828f7fd6e3424b0": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "8a79523b2748baf9b1855412160886880dccee5ba56b3eaea4b74c1ef15004c1" - } - }, - "ebfef09f31ba1666ce3e31a60513412185af79f1ab3cbd6040aeb5a988f7c3b9": { - "keytype": "ed25519", - "scheme": "ed25519", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "bca24fece172faca39582b5b930564a0955062795201bfb0e5bb1d8ab273ff5e" - } - } - }, - "roles": { - "root": { - "keyids": [ - "ebfef09f31ba1666ce3e31a60513412185af79f1ab3cbd6040aeb5a988f7c3b9" - ], - "threshold": 1 - }, - "snapshot": { - "keyids": [ - "a0986ef13d95e0885948fa0aa9dc6d15efe784e8738baa9007754302fc5223af" - ], - "threshold": 1 - }, - "targets": { - "keyids": [ - "aef156f2d58836ca19117799ae800f21447401858b8291ab8828f7fd6e3424b0" - ], - "threshold": 1 - }, - "timestamp": { - "keyids": [ - "68112049b36f72691c2ada4409c273fae84a98cb19abb7e348025d8374b9d293" - ], - "threshold": 1 - } - }, - "consistent_snapshot": true - }, - "signatures": [ - { - "keyid": "ebfef09f31ba1666ce3e31a60513412185af79f1ab3cbd6040aeb5a988f7c3b9", - "sig": "e1177d9464a0b3720c927ddba90e665c3e547c1edbec01831ac761aabc0105cb6a7dbe2887cee94da4fbde385e461df207bcca1e249393f2bde89465fc014b08" - } - ] -} \ No newline at end of file diff --git a/pkg/reconciler/trustroot/testdata/tufRepo.tar b/pkg/reconciler/trustroot/testdata/tufRepo.tar deleted file mode 100644 index bbef6cdbdf4778d7d2d141f1fe2c506bd6a012b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3021 zcmV;;3o`T{iwFP!00000|Lk08QyWRr_GkSHg0J(Am05R9#C&KS76Jqa#QDaCqfXt2 zbs(XQ`R^MoZhYWbK=!QriLfPgx4W`4vofEq%cUX+t<1k6DAwcBsfWH&fK=}t1e-L_c z{g1lgYH~B!#wPWeb031%ih>QmB}Y$)bHKqc$*s@?d?20*bk+wWNOZ`I5U3>O`u0Yv z+ulkZJmDPmYPeK<*9!eFo#=+Ew*S~Nlh#MmYYxZlX{(=#_W!HX8=I4sAza*Cz52;4v(rgtaB#^z{ddu^8v2HFZjbysOSxoEO75h1R=pl?G5vUdS%gh z&!ne;{KU$#gg7P{)!ri08dPx6XkZ|W2Th5i)N2h|8O0<<9)lzlBJ+{CXnpuxS;B$T zlzZo$XHGaJ0(vL4q6_sU?#>N(F){Vxk12 zB(s(V;(`>!8m+wciUnHREQOPQD?>i>K!I$hI zq6&m^g@&S+R;1jF1FDF^JB?f`BB-$107WH51mc9Y9uZxDG)xCimC%+jFAP)62yRJ? zMmP&APx>pj@vq!pKbipj;&{aWZ$0ys-vOTC|A?GC;r{{|&G`R1@D2Wd7pLCf9iShk zcLeJ;VXVCFK34a_pgC=&x1~bd1lW`#+~?hUE|AKD2+~`kICs%9#=MeXSY&B**8oas zl*R`J2xtQV61Y}ESkTsRN2NE6S`q{I)&&uK6prXfiF76e)W#DN(ds~>CqgJ;y&x%3 zkTy+L{)Q&p2>L)NcX{AD1O1DW^rJvemG@i-9jF8%fLN*oQm-IDbPlBfr2;tajMdyB zjocv$X{16CHH1ijOQ1SX3|?@l0}5{x5zaehi8IdmNVo{rTPlJzn(3$mc83HfABFyxqQ6wn2Y&!& z)AwBu+|#H9(HfN&9MczH%_tHnxHK9q;oKX|39}|LE-Xrh#6=3o4T~X$2ue|IEuzwr z5~Uf}k$TB23&a}-jB6`{qaskIqi2i-_Bi;v*6gp*!GZkb}}UDQg& z$fA!93`Ph{F&+#MOSqyOP`*TsA6Ae58j2i;?r{7PZxmXme&wz9V60Vrh1ymRjl}b5S|u+yjxIL$Dqq zdKW^JJX-DfOVoInKK#qH_&2!Dix&bOaUCTjHAYz~WmKAQDx8i)pd!M61hm$?88@{U@vcWc8m6>%#;7|3=lH z|Em52z%u^74rKM8tp1bLe|}v&i2pa!@b$;|`~BZcFrM{)uLGakKYw5rTfz7+p1gEBQtIN`2|P#(N(qgw`IZHkJi%v zF4kf8XZSx5@r3^qi89;&u@?NEqEN0>_cjiW3%k|g#!mTsbud@^(4Cj(=UY3&i|Voi z#iV{-z4=~B>aJ98CgnnFGBoqz)k27}QGBp_u`PS&pZ1qKQRP0>4|=DS4{E^nW{Z=( zT4Om=d7Aic-1_Y}?hkub_w#$Ld3ABH-53nw#i=VLa#U>0|NJwzlA*j;`b?}Jt+D?t zJe&Vtqb1g)Oc2e*T~zi(;DX8jvs9cF)S{ZBq!|06{;4IyO0oE`k zS{I$dMXgY)6O7NwP!}<_PEzQ?hLo&* zwX;zN{8=)p{|X??!jH*DXz9*gtCG<)qr!Z^qK(l+a_*ZZ1{*vbfCeeCjtJ zGN;cB`kb+kI>mxAcg8NfzOftS-CEMUHFoiwl;@|7q)}+PTTN99soJi;*9wLCUNK3I zX0rpmH{7~BVAcI{fA{>XSvl?0^E-!7D~=W?JuIAIFK2tj$=2lPYG-=5cN_#gI>>b<@7m|W_@0opO;P!p^?h-8J}POkx%#^XPB-3 ztOY3KSNY$M?qhv_*NL0azqi6V%>K96e<%RY`ajo#7q0(=;ufpz(i2f;SC?SU^XIz- zHxaS5T|CcSc6R!gwa&y1PH`|CkP0i|i60FU;Z9rI-QP^9*+=@7hAK}Y2R>S&Y}z=%fen9|a6X&sAE zI;)js6ubZiQ85Fa@Tky9t0XbXSg8Xe-g`@;HOfZ}NI`H-2=f5ppBNM~WXO==7r=i4 P00960Ekyfo09*h7y5I3) diff --git a/pkg/reconciler/trustroot/testdata/tufRepoWithCustomTrustedRootJSON.tar b/pkg/reconciler/trustroot/testdata/tufRepoWithCustomTrustedRootJSON.tar deleted file mode 100644 index 2a4c1b73a34fbe323c025ae19fd109c9a23ba26a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3528 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 CrUTIc diff --git a/pkg/reconciler/trustroot/testdata/tufRepoWithTrustedRootJSON.tar b/pkg/reconciler/trustroot/testdata/tufRepoWithTrustedRootJSON.tar deleted file mode 100644 index b266a83128eb3be15a07b1142359864cb181941b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3524 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 diff --git a/pkg/reconciler/trustroot/trustroot_test.go b/pkg/reconciler/trustroot/trustroot_test.go index fdeb3b499..20de37550 100644 --- a/pkg/reconciler/trustroot/trustroot_test.go +++ b/pkg/reconciler/trustroot/trustroot_test.go @@ -48,6 +48,7 @@ import ( "knative.dev/pkg/controller" "knative.dev/pkg/system" + "github.com/sigstore/policy-controller/internal/tuftest" . "github.com/sigstore/policy-controller/pkg/reconciler/testing/v1alpha1" "github.com/sigstore/policy-controller/pkg/reconciler/trustroot/resources" "github.com/sigstore/policy-controller/pkg/reconciler/trustroot/testdata" @@ -160,30 +161,48 @@ var marshalledEntryFromMirrorFS = string(canonicalizeSigstoreKeys(testdata.Get(" var rekorLogID = string(testdata.Get("rekorLogID.txt")) var ctfeLogID = string(testdata.Get("ctfeLogID.txt")) -// validRepository is a valid tarred repository representing an air-gap -// TUF repository. -var validRepository = testdata.Get("tufRepo.tar") - -// IMPORTANT: The next expiration is on 2027-01-28 -// rootJSON is a valid root.json for above TUF repository. -var rootJSON = testdata.Get("root.json") - -// validRepositoryWithTrustedRootJSON is a valid tarred repository representing -// an air-gap TUF repository containing trusted_root.json. -var validRepositoryWithTrustedRootJSON = testdata.Get("tufRepoWithTrustedRootJSON.tar") - -// IMPORTANT: The next expiration is on 2027-01-28 -// rootJSON is a valid root.json for above TUF repository. -var rootWithTrustedRootJSON = testdata.Get("rootWithTrustedRootJSON.json") - -// validRepositoryWithCustomTrustedRootJSON is a valid tarred repository representing -// an air-gap TUF repository containing custom_trusted_root.json. -var validRepositoryWithCustomTrustedRootJSON = testdata.Get("tufRepoWithCustomTrustedRootJSON.tar") +// tufRepos holds the tarred air-gap TUF repositories used by TestReconcile +// along with their matching root.json files. +type tufRepos struct { + repository, root []byte + withTrustedRoot, rootWithTrustedRoot []byte + withCustomTrustedRoot, rootWithCustomTrustedRoot []byte +} -// rootWithCustomTrustedRootJSON is a valid root.json for above TUF repository. -var rootWithCustomTrustedRootJSON = testdata.Get("rootWithCustomTrustedRootJSON.json") +// newTUFRepos generates the TUF repositories used by TestReconcile. They are +// built at test time rather than committed to testdata because TUF metadata +// expires, which would otherwise break the suite roughly every six months. +// +// The target names matter: sigstore derives each target's usage from its +// filename, and the reconciler reads that usage back out. The keys and +// certificates they wrap stay committed, since they are valid for ten years +// and the golden files are derived from them rather than from the TUF +// metadata. +func newTUFRepos(t *testing.T) (repos tufRepos) { + t.Helper() + + trustedRoot := testdata.Get("marshalledEntry.json") + + repos.repository, repos.root = tuftest.NewRepo(t, []tuftest.Target{ + {Name: "rekor.pem", Bytes: testdata.Get("rekorPublicKey.pem")}, + {Name: "ctfe.pem", Bytes: testdata.Get("ctfePublicKey.pem")}, + {Name: "fulcio.pem", Bytes: testdata.Get("fulcioCertChain.pem")}, + }) + repos.withTrustedRoot, repos.rootWithTrustedRoot = tuftest.NewRepo(t, []tuftest.Target{ + {Name: "trusted_root.json", Bytes: trustedRoot}, + }) + repos.withCustomTrustedRoot, repos.rootWithCustomTrustedRoot = tuftest.NewRepo(t, []tuftest.Target{ + {Name: "custom_trusted_root.json", Bytes: trustedRoot}, + }) + return repos +} func TestReconcile(t *testing.T) { + repos := newTUFRepos(t) + validRepository, rootJSON := repos.repository, repos.root + validRepositoryWithTrustedRootJSON, rootWithTrustedRootJSON := repos.withTrustedRoot, repos.rootWithTrustedRoot + validRepositoryWithCustomTrustedRootJSON, rootWithCustomTrustedRootJSON := repos.withCustomTrustedRoot, repos.rootWithCustomTrustedRoot + table := TableTest{{ Name: "bad workqueue key", // Make sure Reconcile handles bad keys. diff --git a/pkg/tuf/repo_test.go b/pkg/tuf/repo_test.go index 8ba881fb8..23946bf69 100644 --- a/pkg/tuf/repo_test.go +++ b/pkg/tuf/repo_test.go @@ -17,9 +17,8 @@ package tuf import ( "bytes" "context" - "encoding/base64" + "encoding/json" "errors" - "fmt" "net/http" "net/http/httptest" "os" @@ -28,9 +27,8 @@ import ( "testing/fstest" "time" - "github.com/theupdateframework/go-tuf" + "github.com/sigstore/policy-controller/internal/tuftest" "github.com/theupdateframework/go-tuf/v2/metadata" - "knative.dev/pkg/logging" ) const ( @@ -67,42 +65,35 @@ c70LfiFo//8/QsvyjLIUtEWHTkGeuf4PpbYXr5qpJ6tWhG2MARxdeg8CAwEAAQ== MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEF6j2sTItLcs0wKoOpMzI+9lJmCzf N6mY2prOeaBRV2dnsJzC94hOxkM5pSp9nbAK1TBOI45fOOPsH2rSR++HrA== -----END PUBLIC KEY-----` - - // validRepository is a valid tar/gzipped repository representing an air-gap - // TUF repository. - validRepository = `H4sIAAAAAAAA/+xcaVMbSdL2Z/0Kgq/zjpVZd03EfGgdFgIECMT5xoajTt2HdSChjfnvGy1ufOBZQPas+4mwJXW3u7IzszKfrszyOIyGk/Z0OL5692YAAJCcp58oOXn4eYt3yAklKBgh8h0gRcnebfC3E+kes8nUjN8BDEKv963rnjt/+yC3n/8Q3Ns/j+/Hw+H0fWcyHLzuGKk+BGNfsz8jgE/sz4ng7zbWosRf3P7/zm1sTtrNQfCbf2z8O7exsflxejUKm39sbKbesPl/6aHJKLiPl2E8aQ8H6Rl8D9cn7o/h6ndYjNrjMEmvIUDk74C/E9VA+QcVfxB6cf2PuuFqcjPYxib1mnMfgtaKgg3AJUdPiHPAwTDJrdAEfCCeCnTWByNIdMiMEKgk5fr2Rqvb3koePOEc9Wq4VHzXCv0vneiGq7b/2DKT1kfTaw7H7Wmrn4r2/6vTG5uTliFc3Fy9+smRbK5+/ev+FpemdyfFxuZoZnttlw4G2gpCddCEg0NDFSEugIs0OiTSEiFtgMCJicECM1EBE8Sx6AQaxQi7Huiv9O+/VqNtcs4EM5QZmipNcooUGY0sqOgNkyxw70kEIcFI7Q3zhHpl0XnPpKeB/MTKsoQ4JhnV3iLTGlRw0WhgIIwHTYmj1GsNWhnFJEYegyQUIvORuWh9+FxZwoHUlHvnnWdOOCUCeie15CIKFBqE98pwz4130gXpAlXc6GAIQR3B/MTKip477pQXQSB6Lr2x6DSzSBT3xHBupfYalYTIIpiouGcWNYk+WOkJ/1xZESAgl0ZyFaSPgUSpJZM+SgWCKmKiBKMks5YG4oIUxBqtvPaKBW18/ImVpUlgzESuiORKMK54dDwQIoR3IH0wTqJ1PkovHffGCu4FCnSGp2nK2ofKyt0obHM87IX7ILaKlA810PYP5X/xtH30qNPWOExaw14asPGBCScDM5q0vinIi4PtdwkyNeNmmE6+IceLp+b3ydHuh8nU9EffkOTFfv+MJHcO44aDSXsyDYPpxweGmo5nIbe6YpWEzXR2nT1T8a5dayVx6sYvdqIbL2k307s5pgSoYJijQqYhAbVmBK01VBuUnGrLogqEcoWGopdWpNyMA6qIwjuNXCsiqLDaAEUlRQTihLYeEV2ANEwHT5XQljAvDFPMCG5JjEY4CytP+iu38a/cXz+aAP3ieMT/bz3zld8BnuH/iJw84f8CGc34/zrwDf5/F6fe4B2gH6Z3BOs2Yay87j5W98KgOW2lt6VC3ATYlCPc5917LpDKgwjSW6I4d9EyxrySYFhkaXTXAEpplMYHBghoAwESuFZCWNDWcEelN4xaEgP31gURfBSO6EiJdCYE1MYTE9HqQKMnQihhvSApUZA2TVb+hibcCPpAKXd54DsD/YuT9KNAT5X3XAgdqAQigFiqBFKvIEB0DqUBpQI47ozV3CGN3CEKaaRCETWESIBLoDpQDF5wqQMKbxQRzjtliTPO+Ui4CNZbKoI1mumoA3jqIwbgWaD/ufEo/j+ciK84xjPxn3Ainq7/SJHF/7XgG/H/lse/Qfh/8oqw6aYxvB+F/peiv1TPB38AZtP3XQ/c06gF0RqVFhzTF1/NKBIpuDQaLeHcG80JsyYyLixKhsRFCsgNiyGGCEpRbi2i0pISpRnXkXrJLAvEGweMayuDoyJQpqNnjAX2OPi72WQ67D8UtN2cTIfjcH9oY3M2Mc2VmouND+Xbd9uNzcnUTGcr7SVu2r4M92dm43Z6+Hqo6/fRx6/ws55rD7+mRQDyHWr0kgKjNEhiQqAUDShNAw/aCyYZQ7RaCPTANJUGmUEMSkjCmdJEa+kpD0ApVx6oiAyEk15Ebpli6F3USsXIoo8ODEYITDEeDBBLuKIcUL5EjR9Wj/8aihyH7nD8Em8UWqd+5a32YKxVkXsqubBEoAtoDRFWOUKUFCFNxMxLp12wlAjKnKRGUk29NYp6qhwVVFHrUEUQllrqInNROeTcA5GgQ7AeInoXQpQ8Wu35S9R4mD78y7T4N7jOixcCHnEdJSQ1RlkqZVSacCqcjkSgIsKA5BKlcN5HZiUVBKWkKAxGbU0EDoGC5YKAs1yZ6Lg2WmE6yZFoL4hHgoQxCN46bhV46WkMlJAQnWNCUAvun8h1HuT/N6r+/Ff1HyQiy//rQFb/yeo/P4OysvpPVv/J6j9Z/Ser/2T1nwzrwwP+/0bVn/+q/gPAMv6/DmT1n6z+k9V/fl08iP830/D1x/j7/d9MMJ71f68Dn9s/f2AWW8H4MJ68h/xrVFaequbJJxAmHvkCEMaYeLexWIcCbu2/jrF+QqAQGyMzbf35BUf40VW197dlwdyP1tL/LtZh9mf4PxDxhP8TiTLb/7EW/J6iUK5U9zYOjgu71eLGTvl8dTBX+9Cdl+fnWzvDi+qyA8Wkfl69+V5K6q5UbyblQmsyNPPi1UKIslF8sHuxLXvnn/aXO1HlPtUPeqeVD2rA9maLk71aYzlTeZr4btz67TjuD3tW9/N7rXl1cbDdGAzixakrJXJUbMz//DO3kqG8V/pMrB+tsf8tPJv/X6Ml4Ln8z+Gz/M9Jlv/XARTq6/n/B7eDvL9vaclm/RthHWZ/bv1PPl3/I0Jk+X89eJD/i+XDRvVDtZg0yjcEoFotHHSKBb6TlJJmuZj+qSXDSrH4qXJUY7qQ1Iq1BBbFZbJdaO6dFJJaI+nvtWoFdlZqVEmuVnJX+6UyrS3Pr2q94VmpUSNPjs1Ly/JeLZlUEjwuJ4ta2VZOWhedwkWtUKvkClfXIyXN8t2oyby8lUA1KezFLa2vjshvB+2eLC5GO4fbJ3RZnu82D+lZYf+3yriTazuXFHQFZzjFBb8YdPufjhATurVnRuX2uNc97gy35NlglN8/xONlJVZOO7VlsqglLJXI50rzciE/r5eTebUyLyUxfc6to1q5UkpOm4V6ez47UB1l7YeD+jgmybk4m12Ud08v26Vkp9Bsfsq1up39g3q9lDT3tpNSoZK0y4ledpaznUZ7PNOjs8PuKd2xn4b9bj6/fYJqi+Rddb7frgytvqwWc9V6MZ/vRne41y1NYdg/auLlpNrYb8bS5JTQY9FuLlX9srjM73zqNx8yp88M+pyxjzrFgjJ3xi68vrEb5d1a0r01dvGclOenjaRRaLpbTVULqdquv9cKhfleMUmOZrMDvTfcujxg1f1yf/f8rFk57dTzO4dQK44WJz0snGEvZ3vFydbk6Gpnerh3HAQ9Oq9WT7YXhwYKfgDDIZr62ZGeDuJFuTDpH8ghVOf1UrK/smldFXJJVOX0OZNmfV6an5dODqGR1LfyheR4nqSOsEz89cWs/KFZP65Odjv5ar5CFg3YofMkV9nvyf5hEcesOS827+hy6j2loySZH9arzbN2+6wjjpuNw+Mdf1xbbqlut7ivuny7upPjojOadPqXsLU9X/RZsVovVtvL/GXbXhwvagx3SLmbn5eOO1XVLe/uY3JcPQktHpdYKh0l3zT+l+f/c/zvNXoZn1//kU/4H2eYrf+sBSjkV/nfj+5jfX/XiZvRv7fCOsz+t9d/KADP+j/Xgheu/4TiFqvt0hNRZleXYM7z57sFNxGxtV08z112dvqXtc7EDU5xMByQCivhiR8NE+FOW9vd30yp2V3sRNWW27tnfkBqe7L/qbZ/xerZ+s+68Pn8f/0W8Gfm/5f2fwmS9X+sBdn+r2z/V7b/K9v/le3/+uX3f931S695/zcip5/9/x8cs/y/Dnwr/9/1z791A+ijzuMvRF2B3xN1pfZKG+8Jg0hFVC5ipEgRQyQcpUDhwGqvUCknNSEcfJTECqmU9lJ6g5EZC9oZAR4ZUUFzD0xIA1EHah2linoXWIzcQIxgUyKgqY4KLYdXawB98Z6ER0GRKIlUOMmU0jZQxnjkjlrpELxTQUflAupolBbAiEbtmNLScmkdi1E6q5VlFLwABlExA57wqNBJqyQxylipAyPee0uiNcpHDEhBcE0I8wDhnxgUM2TIkOEXwH8CAAD//y1E+28AXAAA` - - // IMPORTANT: The next expiration is on '2027-01-28T17:36:23Z' - // To regenerate rootJSON and the matching validRepository above, run - // `make generate-testdata` and base64-encode the resulting - // pkg/reconciler/trustroot/testdata/root.json and tufRepo.tar respectively. - rootJSON = `ewogInNpZ25lZCI6IHsKICAiX3R5cGUiOiAicm9vdCIsCiAgInNwZWNfdmVyc2lvbiI6ICIxLjAiLAogICJ2ZXJzaW9uIjogMSwKICAiZXhwaXJlcyI6ICIyMDI3LTAxLTI4VDE3OjM2OjIzWiIsCiAgImtleXMiOiB7CiAgICIzZDk1NWRlZTk5ODMwYmUwNTc1MWQyMmNjMDUwYTQ3NWI2OTIwZGUyZDM2MWNiZGVhNjJmYzE0YTY2MTg3MzU5IjogewogICAgImtleXR5cGUiOiAiZWQyNTUxOSIsCiAgICAic2NoZW1lIjogImVkMjU1MTkiLAogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICIwOWI2MjM5ZTkyNTBjMWEzODIyY2UwY2YzZmMxMjdiMjY3YmUwZTUyYWZlYjA0YWY4MDQ2MmM0ZmM2MWE4NDI0IgogICAgfQogICB9LAogICAiNTU0NjRhMzRhMzk1NWQ3NTMxMzE0M2Y0ZThmZGE0NzRlNWRkMmYwNjcwYTc5ZGE0ZDIzZDhiMWNkZDQ3ZDNlMiI6IHsKICAgICJrZXl0eXBlIjogImVkMjU1MTkiLAogICAgInNjaGVtZSI6ICJlZDI1NTE5IiwKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiYjIyYzQ3NDM5ZGIxNDk5MDhlY2ZhOTA0MDZhZDA5MzJjMzNkOTkwOThhODQ3MWY1ZmU3MjMwZjRkZjRjZmJkZSIKICAgIH0KICAgfSwKICAgIjZjMDc5MzVkY2RjZDRjNmM4NmUxZGM3OTc1NmY2MTY5MDZkZDhhNWQ1YWRjN2NlN2NlMzg1YTllYTIyMTlmMGEiOiB7CiAgICAia2V5dHlwZSI6ICJlZDI1NTE5IiwKICAgICJzY2hlbWUiOiAiZWQyNTUxOSIsCiAgICAia2V5aWRfaGFzaF9hbGdvcml0aG1zIjogWwogICAgICJzaGEyNTYiLAogICAgICJzaGE1MTIiCiAgICBdLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogImZkNWM1YzhkNmU2MTFkNTdkYWIxYzk0YjEyODVkMmE1NWI3OWQ5MTg3MGY0ZjBhZjg1ZDRiMTkyZmRlYjdkMjUiCiAgICB9CiAgIH0sCiAgICJmMDBlMTU3YTc1OGU3ZGZlMmY3OTc0N2RmNzgwNjM4MmFmNzBhODc0YmIzZTJjZTc2MmJhOThkOWQ4NGU5YWRmIjogewogICAgImtleXR5cGUiOiAiZWQyNTUxOSIsCiAgICAic2NoZW1lIjogImVkMjU1MTkiLAogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICI5MmU0NGFmNTgyNzU4NjQ1ODVmYzVlMjI2NmRjMDdkZWFjNzFiY2RmN2Q3YzVkYWI2NWQ2MTYxY2E1MDAxN2JiIgogICAgfQogICB9CiAgfSwKICAicm9sZXMiOiB7CiAgICJyb290IjogewogICAgImtleWlkcyI6IFsKICAgICAiNTU0NjRhMzRhMzk1NWQ3NTMxMzE0M2Y0ZThmZGE0NzRlNWRkMmYwNjcwYTc5ZGE0ZDIzZDhiMWNkZDQ3ZDNlMiIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMQogICB9LAogICAic25hcHNob3QiOiB7CiAgICAia2V5aWRzIjogWwogICAgICIzZDk1NWRlZTk5ODMwYmUwNTc1MWQyMmNjMDUwYTQ3NWI2OTIwZGUyZDM2MWNiZGVhNjJmYzE0YTY2MTg3MzU5IgogICAgXSwKICAgICJ0aHJlc2hvbGQiOiAxCiAgIH0sCiAgICJ0YXJnZXRzIjogewogICAgImtleWlkcyI6IFsKICAgICAiNmMwNzkzNWRjZGNkNGM2Yzg2ZTFkYzc5NzU2ZjYxNjkwNmRkOGE1ZDVhZGM3Y2U3Y2UzODVhOWVhMjIxOWYwYSIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMQogICB9LAogICAidGltZXN0YW1wIjogewogICAgImtleWlkcyI6IFsKICAgICAiZjAwZTE1N2E3NThlN2RmZTJmNzk3NDdkZjc4MDYzODJhZjcwYTg3NGJiM2UyY2U3NjJiYTk4ZDlkODRlOWFkZiIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMQogICB9CiAgfSwKICAiY29uc2lzdGVudF9zbmFwc2hvdCI6IHRydWUKIH0sCiAic2lnbmF0dXJlcyI6IFsKICB7CiAgICJrZXlpZCI6ICI1NTQ2NGEzNGEzOTU1ZDc1MzEzMTQzZjRlOGZkYTQ3NGU1ZGQyZjA2NzBhNzlkYTRkMjNkOGIxY2RkNDdkM2UyIiwKICAgInNpZyI6ICJjNDg2MDhlYTRjMzY3N2QyNTE5OTQyMWJiYTM5YTE3NTM5YjRmOGUyMzU4MWEzMWQ3YjY2NDIyNTAxOGYxNmRjOTE1OTgyNjM2YjlhMDMxODc2ZjAyYzY5YmQxMTFjZTA4YTg0ZWQzODY5YjI0ZDZhNDg0YTY1YjJmZmE2Y2IwOSIKICB9CiBdCn0=` ) -func TestCompressUncompressFS(t *testing.T) { - files := map[string][]byte{ - "fulcio_v1.crt.pem": []byte(fulcioRootCert), - "ctfe.pub": []byte(ctlogPublicKey), - "rekor.pub": []byte(rekorPublicKey), +// testTargets are the targets used by the tests in this file. The names match +// the ones a real sigstore TUF repository publishes. +func testTargets() []tuftest.Target { + return []tuftest.Target{ + {Name: "fulcio_v1.crt.pem", Bytes: []byte(fulcioRootCert)}, + {Name: "ctfe.pub", Bytes: []byte(ctlogPublicKey)}, + {Name: "rekor.pub", Bytes: []byte(rekorPublicKey)}, } - repo, dir, err := createRepo(context.Background(), files) - if err != nil { - t.Fatalf("Failed to CreateRepo: %s", err) - } - defer os.RemoveAll(dir) +} + +func TestCompressUncompressFS(t *testing.T) { + targets := testTargets() + // Generate into a directory rather than asking the helper for a tarball, so + // that the compression exercised below is this package's own. + local, dir := tuftest.NewRepoDir(t, targets, tuftest.WithConsistentSnapshot(false)) var buf bytes.Buffer fsys := os.DirFS(dir) - if err = CompressFS(fsys, &buf, map[string]bool{"keys": true, "staged": true}); err != nil { + if err := CompressFS(fsys, &buf, map[string]bool{"keys": true, "staged": true}); err != nil { t.Fatalf("Failed to compress: %v", err) } - os.WriteFile("/tmp/newcompressed", buf.Bytes(), os.ModePerm) dstDir := t.TempDir() - if err = Uncompress(&buf, dstDir); err != nil { + if err := Uncompress(&buf, dstDir); err != nil { t.Fatalf("Failed to uncompress: %v", err) } // Then check that files have been uncompressed there. - meta, err := repo.GetMeta() + meta, err := local.GetMeta() if err != nil { t.Errorf("Failed to GetMeta: %s", err) } @@ -122,81 +113,11 @@ func TestCompressUncompressFS(t *testing.T) { if err != nil { t.Errorf("Failed to read the roundtripped rekor %v", err) } - if !bytes.Equal(files["rekor.pub"], rtRekor) { + if !bytes.Equal([]byte(rekorPublicKey), rtRekor) { t.Errorf("Roundtripped rekor differs:\n%s\n%s", rekorPublicKey, string(rtRekor)) } } -func createRepo(ctx context.Context, files map[string][]byte) (tuf.LocalStore, string, error) { - // TODO: Make this an in-memory fileystem. - // tmpDir := os.TempDir() - // dir := tmpDir + "tuf" - dir := "/tmp/tuf" - err := os.Mkdir(dir, os.ModePerm) - if err != nil { - return nil, "", fmt.Errorf("failed to create tmp TUF dir: %w", err) - } - dir += "/" - logging.FromContext(ctx).Infof("Creating the FS in %q", dir) - local := tuf.FileSystemStore(dir, nil) - - // Create and commit a new TUF repo with the targets to the store. - logging.FromContext(ctx).Infof("Creating new repo in %q", dir) - r, err := tuf.NewRepoIndent(local, "", " ") - if err != nil { - return nil, "", fmt.Errorf("failed to NewRepoIndent: %w", err) - } - - // Added by vaikas - if err := r.Init(false); err != nil { - return nil, "", fmt.Errorf("failed to Init repo: %w", err) - } - - // Make all metadata files expire in 6 months. - expires := time.Now().AddDate(0, 6, 0) - - for _, role := range []string{"root", "targets", "snapshot", "timestamp"} { - _, err := r.GenKeyWithExpires(role, expires) - if err != nil { - return nil, "", fmt.Errorf("failed to GenKeyWithExpires: %w", err) - } - } - - targets := make([]string, 0, len(files)) - for k, v := range files { - logging.FromContext(ctx).Infof("Adding %s file", k) - if err := writeStagedTarget(dir, k, v); err != nil { - return nil, "", fmt.Errorf("failed to write staged target %s: %w", k, err) - } - targets = append(targets, k) - } - err = r.AddTargetsWithExpires(targets, nil, expires) - if err != nil { - return nil, "", fmt.Errorf("failed to add AddTargetsWithExpires: %w", err) - } - - // Snapshot, Timestamp, and Publish the repository. - if err := r.SnapshotWithExpires(expires); err != nil { - return nil, "", fmt.Errorf("failed to add SnapShotWithExpires: %w", err) - } - if err := r.TimestampWithExpires(expires); err != nil { - return nil, "", fmt.Errorf("failed to add TimestampWithExpires: %w", err) - } - if err := r.Commit(); err != nil { - return nil, "", fmt.Errorf("failed to Commit: %w", err) - } - return local, dir, nil -} - -func writeStagedTarget(dir, path string, data []byte) error { - path = filepath.Join(dir, "staged", "targets", path) - if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { - return err - } - - return os.WriteFile(path, data, 0644) -} - func TestFsFetcherNotFound(t *testing.T) { testFS := fstest.MapFS{ "existing.json": &fstest.MapFile{Data: []byte(`{"hello":"world"}`)}, @@ -247,14 +168,7 @@ func TestFsFetcherMaxLength(t *testing.T) { } func TestDownloadTargetFromSerializedMirror(t *testing.T) { - repo, err := base64.StdEncoding.DecodeString(validRepository) - if err != nil { - t.Fatalf("failed to decode validrepository: %v", err) - } - root, err := base64.StdEncoding.DecodeString(rootJSON) - if err != nil { - t.Fatalf("failed to decode rootJSON: %v", err) - } + repo, root := tuftest.NewRepo(t, testTargets()) tufClient, err := ClientFromSerializedMirror(context.Background(), repo, root, "targets", "/repository/") if err != nil { t.Fatalf("Failed to create client: %v", err) @@ -278,14 +192,7 @@ func TestDownloadTargetFromSerializedMirror(t *testing.T) { } func TestClientFromSerializedMirror(t *testing.T) { - repo, err := base64.StdEncoding.DecodeString(validRepository) - if err != nil { - t.Fatalf("failed to decode validrepository: %v", err) - } - root, err := base64.StdEncoding.DecodeString(rootJSON) - if err != nil { - t.Fatalf("failed to decode rootJSON: %v", err) - } + repo, root := tuftest.NewRepo(t, testTargets()) tufClient, err := ClientFromSerializedMirror(context.Background(), repo, root, "targets", "/repository/") if err != nil { t.Fatalf("Failed to unserialize repo: %v", err) @@ -299,24 +206,50 @@ func TestClientFromSerializedMirror(t *testing.T) { } } -func TestClientFromRemoteMirror(t *testing.T) { - files := map[string][]byte{ - "fulcio_v1.crt.pem": []byte(fulcioRootCert), - "ctfe.pub": []byte(ctlogPublicKey), - "rekor.pub": []byte(rekorPublicKey), +// TestClientFromSerializedMirrorExpired guards against the fixtures above being +// made "fresh" by accidentally disabling expiry validation: a repository whose +// metadata has expired must still be rejected. go-tuf refuses to sign metadata +// that is already expired, so this signs a short-lived repository and waits for +// it to lapse. +func TestClientFromSerializedMirrorExpired(t *testing.T) { + if testing.Short() { + t.Skip("skipping expiry test in short mode") + } + + repo, root := tuftest.NewRepo(t, testTargets(), tuftest.WithExpires(time.Now().Add(2*time.Second))) + + // Wait on the expiry recorded in the metadata rather than the one requested: + // go-tuf rounds to the nearest second, so the effective expiry can be up to + // half a second later than asked for. + var doc struct { + Signed struct { + Expires time.Time `json:"expires"` + } `json:"signed"` + } + if err := json.Unmarshal(root, &doc); err != nil { + t.Fatalf("failed to read the generated root.json: %v", err) } - local, dir, err := createRepo(context.Background(), files) + time.Sleep(time.Until(doc.Signed.Expires) + 100*time.Millisecond) + + tufClient, err := ClientFromSerializedMirror(context.Background(), repo, root, "targets", "/repository/") if err != nil { - t.Fatalf("Failed to CreateRepo: %s", err) + // Rejected eagerly, which is fine. + return + } + if _, err := tufClient.GetTopLevelTargets(); err == nil { + t.Error("expected expired TUF metadata to be rejected, got no error") } - defer os.RemoveAll(dir) +} + +func TestClientFromRemoteMirror(t *testing.T) { + local, dir := tuftest.NewRepoDir(t, testTargets(), tuftest.WithConsistentSnapshot(false)) meta, err := local.GetMeta() if err != nil { t.Fatalf("getting meta: %v", err) } rootJSON, ok := meta["root.json"] if !ok { - t.Fatalf("Getting root: %v", err) + t.Fatal("generated repository has no root.json") } serveDir := filepath.Join(dir, "repository") t.Logf("tuf repository was created in: %s serving tuf root at %s", dir, serveDir)