Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions cmd/quarry-client/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/urfave/cli/v3"

"go.amutable.dev/quarry/internal/third_party/funchelpers"
"go.amutable.dev/quarry/internal/tufclient"
"go.amutable.dev/quarry/internal/tufext"
"go.amutable.dev/quarry/internal/xsysupdate"
)
Expand Down Expand Up @@ -220,6 +221,12 @@ func proxyTargetFile(rw http.ResponseWriter, req *http.Request) (Err error) {
return nil
}

// A redirect would send systemd-pull to the repo without this repo's
// request signatures, so targets of a repo with auth are proxied instead.
if info.Repo.Client != nil {
return streamTargetFile(rw, req, info)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite unfortunate, though since only the method and URI are included in the signature I guess we can eventually pre-compute the signed headers and provide them to sd-pull once it switches to UAPI.16 (and a UAPI.16 extension is added to support it).

@davidstrauss Do we plan to extend the scope of things we would sign for WIMSE/WIT? If we start signing other headers then we would just need to accept that we are going to be proxying data a lot. (Though we could of course choose to proxy based n what headers are needed to be signed...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might need more context, but I wasn't expecting we'd use WIT/WIMSE for repo auth at all. I thought we only wanted to rely on authZ mechanisms that (1) we can delegate to the delivery system itself (mainly for cost, DoS protection, and maybe some basic infrastructure disclosure control) or (2) we can achieve via sealing the data going through Quarry (for load-bearing secret delivery to specific machines).

In short, I assumed our entire model would work fine with publicly readable buckets.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can delegate to the delivery system itself (mainly for cost, DoS protection, and maybe some basic infrastructure disclosure control)

This is the "front door" access control we discussed in January, BTW.


// Redirect to the first target URL.
// TODO(uapi16): Once we get UAPI.16 support into systemd, we can serve a
// manifest that lists every URL candidate as an alternative contents source
Expand Down Expand Up @@ -252,6 +259,37 @@ func proxyTargetFile(rw http.ResponseWriter, req *http.Request) (Err error) {
return errors.New("no fetch urls defined for target")
}

// streamTargetFile serves the target's contents, fetched with the repo's own
// HTTP client. The digest is only checked at EOF, after the whole body has been
// written, so a mismatch aborts the connection but cannot take back the bytes:
// the client must check the body against Content-Digest.
func streamTargetFile(rw http.ResponseWriter, req *http.Request, info *tufclient.TargetInfo) error {
ctx := req.Context()

rdr, err := info.Fetch(ctx)
if errors.Is(err, fs.ErrNotExist) {
http.NotFound(rw, req)
return nil
}
if err != nil {
return fmt.Errorf("fetch target %s from repo %s: %w", info.Path, info.Repo.Name, err)
}

rw.Header().Set("Content-Length", strconv.FormatInt(info.Length, 10))
rw.Header().Set("Content-Type", "application/octet-stream")
if digestValue := hashesToContentDigest(info.Hashes); digestValue != "" {
rw.Header().Set("Content-Digest", digestValue)
rw.Header().Set("Repr-Digest", digestValue)
}
_, copyErr := io.Copy(rw, rdr)
if err := errors.Join(copyErr, rdr.Close()); err != nil {
slog.ErrorContext(ctx, "Aborting target response",
"repo", info.Repo.Name, "target", info.Path, "err", err.Error())
panic(http.ErrAbortHandler)
}
return nil
}

// newSysupdateHandler returns a [http.Handler] that impements our sysupdate
// SHA256SUM-based compatibility shim.
func newSysupdateHandler() http.Handler {
Expand Down
51 changes: 51 additions & 0 deletions cmd/quarry-client/http_insecure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"path/filepath"
"slices"
"strconv"
"strings"
Expand All @@ -29,6 +31,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tufmetadata "github.com/theupdateframework/go-tuf/v2/metadata"
"snai.pe/go-varlink"

"go.amutable.dev/quarry/cmd/internal/cliext"
"go.amutable.dev/quarry/internal/ctxext"
Expand Down Expand Up @@ -813,3 +816,51 @@ func TestHTTP_ReleaseLayouts(t *testing.T) {
})
}
}

// startFakeIdentityAgent serves GetWorkloadIdentityToken with a fixed WIT and
// returns the agent's varlink URI.
func startFakeIdentityAgent(t *testing.T) string {
t.Helper()
uri := "unix:" + filepath.Join(t.TempDir(), "agent") //nolint:forbidigo // test code
listener, err := varlink.Listen(uri)
require.NoError(t, err, "listen on fake agent socket")
t.Cleanup(func() { _ = listener.Close() })

var mux varlink.ServeMux
mux.HandleFunc("com.amutable.IdentityAgent.GetWorkloadIdentityToken", func(w varlink.ReplyWriter, _ *varlink.Call) {
_ = w.WriteReply(map[string]any{"token": "wit", "expires_in": 3600})
})
srv := &varlink.Server{Handler: &mux}
go func() { _ = srv.Serve(listener) }()
return uri
}

// Targets of a repo with auth are served by quarry-client itself, fetched with
// the repo's signing client: a redirect would make sysupdate fetch them
// unsigned. The gate in front of the repo refuses any request without a WIT.
func TestHTTPProxyTarget_AuthRepoIsStreamed(t *testing.T) {
srv := testrepo.New(t)
files, _ := publishNightlyImage(t, srv)

repoURL, err := url.Parse(srv.URL)
require.NoError(t, err)
proxy := httputil.NewSingleHostReverseProxy(repoURL)
gate := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Header.Get("Workload-Identity-Token") == "" {
http.Error(rw, "unsigned", http.StatusUnauthorized)
return
}
proxy.ServeHTTP(rw, req)
}))
t.Cleanup(gate.Close)

auth := fmt.Sprintf(`auth = { type = "wimse", audience = "urn:amutable:quarry-worker", varlink = %q }`, startFakeIdentityAgent(t))
block := srv.ConfigBlock("nightly", testrepo.ConfigWithExtraLines(auth))
server := newServer(t, strings.ReplaceAll(block, srv.URL, gate.URL))

resp, body := serverGet(t, server, "/"+usrTarget)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, files[usrTarget], body)
assert.Empty(t, resp.Header.Values("Location"))
assert.Equal(t, sha256Digest(files[usrTarget]), resp.Header.Get("Content-Digest"))
}
11 changes: 11 additions & 0 deletions contrib/quarry-client/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,14 @@ cache_dir = "/var/lib/quarry-client/latest-metadata"
## overridden by setting the following two settings.
#meta_root_url = "https://tufmetadata.example.com/tuf/%m"
#data_root_url = "https://cloud-storage.example.com/repo/%m/foobar"

## Requests to the meta_root_url host can be signed as this machine, for
## servers that only serve a repository to the machine it belongs to. Each
## request then carries the machine's WIMSE workload identity token for
## "audience", from the varlink service at "varlink" implementing
## com.amutable.IdentityAgent (default: amutablectl's, at
## unix:/run/amutable/com.amutable.IdentityAgent), and an RFC 9421 signature
## by a key quarry-client holds in memory and the token binds. Requests to
## other hosts (such as a separate data_root_url) are not signed. "wimse" is
## the only supported type.
#auth = { type = "wimse", audience = "urn:amutable:quarry-worker" }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned when we first talked about this, links make this model a little complicated -- I think we would need to model this more like "providers" and then eventually we can come up with some kind of allow-list policy for providers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To flesh this out a bit more -- I think links would likely need to be able to provide something like "I want a provider with this rough shape of type + audience/user fields" and then the policy would determine whether a link can request such a provider and which one should be used.

The "shape" thing is needed because using provider identifiers is too fragile and allowing all of the bits of a provider to be configured is an obvious security issue (a hypothetical basic auth provider might support getting the user and password from a file and allowing a link to configure it would allow for local file leaks).

The simplest policy would be to only allow a repository to use providers that the linking repository has access to (which would walk back to the "root" set specified in configuration). That might be okay for the first version but that is not really the right model -- ideally you would also want the provider to specify which repositories are allowed to use the auth.

The WIT-signed headers include the whole request URI so for the WIMSE provider this restriction isn't strictly required but I'm just a little apprehensive about letting a repo specify what auth the client should send without any restrictions.

5 changes: 2 additions & 3 deletions internal/httputils/verified_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ const MaxErrorBytes = 128
// the server.
const ErrorReadDeadline = 200 * time.Millisecond

// VerifiedHTTPGet is a wrapper around [http.Client.Do] but the returned
// VerifiedHTTPGet is a wrapper around client.Do but the returned
// [io.ReadCloser] is wrapped in a [hardening.VerifiedReadCloser] so that
// endless read and other attacks are protected against. You *must* check the
// error return from [io.ReadCloser.Close] once you are done reading data and
// before you use it for anything.
//
// TODO: Should we also return the response information...?
func VerifiedHTTPGet(ctx context.Context, url *url.URL, length int64, hashes tufmetadata.Hashes) (_ io.ReadCloser, _ *http.Response, Err error) {
func VerifiedHTTPGet(ctx context.Context, client *http.Client, url *url.URL, length int64, hashes tufmetadata.Hashes) (_ io.ReadCloser, _ *http.Response, Err error) {
Comment thread
cyphar marked this conversation as resolved.
// Make sure the hashes are good before doing the request -- they get
// re-converted later in [tufext.VerifiedReadCloser].
if _, err := tufext.HashesToDigest(hashes); err != nil {
Expand All @@ -53,7 +53,6 @@ func VerifiedHTTPGet(ctx context.Context, url *url.URL, length int64, hashes tuf
return nil, nil, fmt.Errorf("create http request: %w", err)
}

client := http.DefaultClient
res, err := client.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("fetch %s: %w", url, err)
Expand Down
46 changes: 23 additions & 23 deletions internal/httputils/verified_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestVerifiedHTTPGet_HappyPath(t *testing.T) {
u := staticServer(t, http.StatusOK, body)
hashes := tufmetadata.Hashes{"sha256": sha256Of(body)}

rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), u, int64(len(body)), hashes)
rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, int64(len(body)), hashes)
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, http.StatusOK, res.StatusCode)
Expand All @@ -73,7 +73,7 @@ func TestVerifiedHTTPGet_MultipleHashes_AllValid(t *testing.T) {
"sha512": sha512Of(body),
}

rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), u, int64(len(body)), hashes)
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, int64(len(body)), hashes)
require.NoError(t, err)

// Layer order is an implementation detail of HashesToDigest; only the set
Expand Down Expand Up @@ -108,7 +108,7 @@ func TestVerifiedHTTPGet_MultipleHashes_OneWrong(t *testing.T) {
"sha512": make([]byte, sha512.Size),
}

rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), u, int64(len(body)), hashes)
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, int64(len(body)), hashes)
require.NoError(t, err)
_, err = io.ReadAll(rdr)
err = errOrClose(err, rdr)
Expand All @@ -122,7 +122,7 @@ func TestVerifiedHTTPGet_DigestMismatch(t *testing.T) {
require.Len(t, wrong, len(body), "test setup: lengths must match to isolate digest mismatch")
hashes := tufmetadata.Hashes{"sha256": sha256Of(wrong)}

rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), u, int64(len(body)), hashes)
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, int64(len(body)), hashes)
require.NoError(t, err)
_, err = io.ReadAll(rdr)
err = errOrClose(err, rdr)
Expand All @@ -134,7 +134,7 @@ func TestVerifiedHTTPGet_ShortBody(t *testing.T) {
u := staticServer(t, http.StatusOK, body)
hashes := tufmetadata.Hashes{"sha256": sha256Of(body)}

rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), u, int64(len(body))+10, hashes)
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, int64(len(body))+10, hashes)
require.NoError(t, err)

got, readErr := io.ReadAll(rdr)
Expand All @@ -151,7 +151,7 @@ func TestVerifiedHTTPGet_LongBody(t *testing.T) {
// (digest-mismatch takes precedence in verify()).
hashes := tufmetadata.Hashes{"sha256": sha256Of(body[:declared])}

rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), u, declared, hashes)
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, declared, hashes)
require.NoError(t, err)
got, readErr := io.ReadAll(rdr)
assert.Equal(t, body[:declared], got, "read should be truncated to declared length")
Expand All @@ -164,7 +164,7 @@ func TestVerifiedHTTPGet_ZeroLengthEmptyBody(t *testing.T) {
u := staticServer(t, http.StatusOK, nil)
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.NoError(t, err)
got, err := io.ReadAll(rdr)
require.NoError(t, err)
Expand All @@ -179,7 +179,7 @@ func TestVerifiedHTTPGet_ZeroLengthNonEmptyBody(t *testing.T) {
// isn't masked by digest-mismatch.
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.NoError(t, err)
_, err = io.ReadAll(rdr)
err = errOrClose(err, rdr)
Expand All @@ -193,7 +193,7 @@ func TestVerifiedHTTPGet_NegativeLength(t *testing.T) {

// The wrapper does not validate length itself; VerifiedReadCloser surfaces
// ErrInvalidExpectedSize on the first Read/Close.
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), u, -1, hashes)
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, -1, hashes)
require.NoError(t, err)
_, err = io.ReadAll(rdr)
err = errOrClose(err, rdr)
Expand All @@ -203,7 +203,7 @@ func TestVerifiedHTTPGet_NegativeLength(t *testing.T) {
func TestVerifiedHTTPGet_EmptyHashes(t *testing.T) {
u := staticServer(t, http.StatusOK, []byte("anything"))

rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, tufmetadata.Hashes{})
rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, tufmetadata.Hashes{})
require.Error(t, err)
assert.ErrorIs(t, err, tufext.ErrNoSupportedHashTypes) //nolint:testifylint // assert is fine for error path checks
assert.Nil(t, rdr)
Expand All @@ -214,7 +214,7 @@ func TestVerifiedHTTPGet_OnlyUnsupportedHash(t *testing.T) {
u := staticServer(t, http.StatusOK, []byte("anything"))
hashes := tufmetadata.Hashes{"md5": []byte("does-not-matter")}

rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.Error(t, err)
assert.ErrorIs(t, err, tufext.ErrNoSupportedHashTypes) //nolint:testifylint // assert is fine for error path checks
assert.Nil(t, rdr)
Expand All @@ -229,7 +229,7 @@ func TestVerifiedHTTPGet_UnsupportedHashIgnoredAlongsideSupported(t *testing.T)
"md5": []byte("unsupported, should be ignored"),
}

rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), u, int64(len(body)), hashes)
rdr, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, int64(len(body)), hashes)
require.NoError(t, err)
got, err := io.ReadAll(rdr)
require.NoError(t, err)
Expand All @@ -241,7 +241,7 @@ func TestVerifiedHTTPGet_NotFound(t *testing.T) {
u := staticServer(t, http.StatusNotFound, []byte("not here"))
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.Error(t, err)
assert.ErrorIs(t, err, fs.ErrNotExist) //nolint:testifylint // assert is fine for error path checks
assert.Nil(t, rdr)
Expand All @@ -253,7 +253,7 @@ func TestVerifiedHTTPGet_InternalServerError(t *testing.T) {
u := staticServer(t, http.StatusInternalServerError, []byte("boom"))
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.Error(t, err)
assert.NotErrorIs(t, err, fs.ErrNotExist, "non-404 status must not masquerade as ENOENT") //nolint:testifylint // assert is fine for error path checks
assert.Contains(t, err.Error(), "500")
Expand All @@ -268,7 +268,7 @@ func TestVerifiedHTTPGet_ContextCancelledBeforeRequest(t *testing.T) {
cancel()
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

rdr, _, err := httputils.VerifiedHTTPGet(ctx, u, 0, hashes)
rdr, _, err := httputils.VerifiedHTTPGet(ctx, http.DefaultClient, u, 0, hashes)
require.Error(t, err)
assert.ErrorIs(t, err, context.Canceled) //nolint:testifylint // assert is fine for error path checks
assert.Nil(t, rdr)
Expand All @@ -279,7 +279,7 @@ func TestVerifiedHTTPGet_ResponseBodyEqualsReturnedReader(t *testing.T) {
u := staticServer(t, http.StatusOK, body)
hashes := tufmetadata.Hashes{"sha256": sha256Of(body)}

rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), u, int64(len(body)), hashes)
rdr, res, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, int64(len(body)), hashes)
require.NoError(t, err)
assert.Same(t, rdr, res.Body, "res.Body must point at the verified reader, not the raw transport stream")
require.NoError(t, rdr.Close())
Expand All @@ -290,7 +290,7 @@ func TestVerifiedHTTPGet_ErrorBodyCaptured(t *testing.T) {
u := staticServer(t, http.StatusInternalServerError, body)
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

_, _, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
_, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.Error(t, err)
assert.Contains(t, err.Error(), "server response:",
"error must label the captured response body so operators can spot it")
Expand All @@ -305,7 +305,7 @@ func TestVerifiedHTTPGet_ErrorBodyTruncatedAtMaxBytes(t *testing.T) {
u := staticServer(t, http.StatusBadGateway, body)
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

_, _, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
_, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.Error(t, err)
assert.Contains(t, err.Error(), "AAAA",
"a sample of the captured prefix must appear so we know capture happened, not just that the tail was dropped")
Expand All @@ -321,7 +321,7 @@ func TestVerifiedHTTPGet_ErrorBodyQuotedNoRawControlBytes(t *testing.T) {
u := staticServer(t, http.StatusBadRequest, body)
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

_, _, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
_, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.Error(t, err)
msg := err.Error()
assert.NotContains(t, msg, "\x1b", "raw escape byte must not leak into the error")
Expand All @@ -334,7 +334,7 @@ func TestVerifiedHTTPGet_ErrorBodyEmpty(t *testing.T) {
u := staticServer(t, http.StatusInternalServerError, nil)
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

_, _, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
_, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.Error(t, err)
assert.Contains(t, err.Error(), "server response empty",
"an empty error body should be reported as such rather than as a read failure")
Expand Down Expand Up @@ -369,7 +369,7 @@ func TestVerifiedHTTPGet_ErrorBodySlowRollReturnsWithBufferedPrefixOnly(t *testi
require.NoError(t, err)
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

_, _, err = httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
_, _, err = httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.Error(t, err)
assert.Contains(t, err.Error(), "EARLY-CHUNK",
"the prefix buffered before the server parked must be captured")
Expand All @@ -396,7 +396,7 @@ func TestVerifiedHTTPGet_ErrorBodyDeadlineFiresOnHangingServer(t *testing.T) {
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

start := time.Now()
_, _, err = httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
_, _, err = httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
elapsed := time.Since(start)

require.Error(t, err)
Expand All @@ -415,7 +415,7 @@ func TestVerifiedHTTPGet_NotFoundIncludesBody(t *testing.T) {
u := staticServer(t, http.StatusNotFound, body)
hashes := tufmetadata.Hashes{"sha256": sha256Of(nil)}

_, _, err := httputils.VerifiedHTTPGet(t.Context(), u, 0, hashes)
_, _, err := httputils.VerifiedHTTPGet(t.Context(), http.DefaultClient, u, 0, hashes)
require.Error(t, err)
assert.ErrorIs(t, err, fs.ErrNotExist) //nolint:testifylint // assert is fine for error path checks
assert.Contains(t, err.Error(), "artifact 'sha256:cafe' absent",
Expand Down
Loading
Loading