From ad339007203c5d98238cb5b7bd198e039a6fbbba Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Tue, 13 Oct 2026 10:47:42 +0100 Subject: [PATCH] tufclient: skip a repo only when its root.json is missing Any failure to fetch a repo's first root.json skipped the repo. A machine that could not authenticate then updated nothing and still reported success. Now only a missing root.json, which means the repo may not exist yet, skips the repo. Any other failure is an error, so one repo that fails this way stops the whole run. Signed-off-by: Joaquim Rocha Signed-off-by: Aleksa Sarai --- internal/tufclient/client.go | 9 ++++++++- internal/tufclient/client_repos_test.go | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/tufclient/client.go b/internal/tufclient/client.go index cdf68b4..0b51e29 100644 --- a/internal/tufclient/client.go +++ b/internal/tufclient/client.go @@ -89,7 +89,14 @@ func RepoClient(ctx context.Context, cacheDir *pathrs.Root, repoLike tufext.Repo // Fallback to fetch from the trusted root source. rootData, err := repo.RootTrust.FetchRoot(ctx, repo) if err != nil { - return nil, skippableErr(fmt.Errorf("fetch trusted root.json: %w", err)) + err = fmt.Errorf("fetch trusted root.json: %w", err) + // Only a missing root.json means the repository may not exist + // yet. Any other failure (auth, network, server error) must not + // look like an empty repository list. + if errors.Is(err, fs.ErrNotExist) { + err = skippableErr(err) + } + return nil, err } rootFile, err = repoCacheDir.Create(".", unix.O_TMPFILE|unix.O_RDWR|unix.O_NOFOLLOW, 0o644) if err != nil { diff --git a/internal/tufclient/client_repos_test.go b/internal/tufclient/client_repos_test.go index 3a4bce8..bc7cecd 100644 --- a/internal/tufclient/client_repos_test.go +++ b/internal/tufclient/client_repos_test.go @@ -184,6 +184,25 @@ func TestNewClient_SkipsInvalidRemoteRoot(t *testing.T) { require.ErrorContains(t, err, "root_trust root.json is invalid JSON") } +// A remote root_trust fetch that fails for any reason but a 404 (here, the +// server refusing the client) is fatal for NewClient: only a missing +// repository may be skipped. +func TestNewClient_RemoteRootFetchErrorFatal(t *testing.T) { + valid, denied := testrepo.New(t), testrepo.NewBroken(t) + denied.Handle("/1.root.json", http.HandlerFunc(func(wtr http.ResponseWriter, _ *http.Request) { + wtr.WriteHeader(http.StatusUnauthorized) + })) + cfg := testrepo.Config(t, + valid.ConfigBlock("valid-repo"), + denied.ConfigBlock("denied-repo"), + ) + + _, err := tufclient.NewClient(t.Context(), cfg) + require.NotErrorIs(t, err, tufclient.ErrSkippableRepo) + require.ErrorContains(t, err, "bad repo denied-repo") + require.ErrorContains(t, err, "status code 401") +} + // A repository with a non-remote root_trust source that fails to load is a // fatal error for NewClient, never a skippable one -- local sources are meant // to always exist and be valid.