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 bbef6cdbd..000000000 Binary files a/pkg/reconciler/trustroot/testdata/tufRepo.tar and /dev/null differ diff --git a/pkg/reconciler/trustroot/testdata/tufRepoWithCustomTrustedRootJSON.tar b/pkg/reconciler/trustroot/testdata/tufRepoWithCustomTrustedRootJSON.tar deleted file mode 100644 index 2a4c1b73a..000000000 Binary files a/pkg/reconciler/trustroot/testdata/tufRepoWithCustomTrustedRootJSON.tar and /dev/null differ diff --git a/pkg/reconciler/trustroot/testdata/tufRepoWithTrustedRootJSON.tar b/pkg/reconciler/trustroot/testdata/tufRepoWithTrustedRootJSON.tar deleted file mode 100644 index b266a8312..000000000 Binary files a/pkg/reconciler/trustroot/testdata/tufRepoWithTrustedRootJSON.tar and /dev/null differ 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)