Skip to content
Closed
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
18 changes: 16 additions & 2 deletions docs/doctoring/trusted-uv-lock-materialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ The implementation therefore:
11. rejects every nonempty export unless every logical line is an exact normalized
package `==` pin followed only by complete SHA-256 hashes; and
12. exposes only generated requirements files and a source manifest to the later
networkless coverage environment.
networkless coverage environment;
13. sends a fixed repository-owned `User-Agent` value on the archive request so
the release host does not reject Python's default `urllib` identity, without
allowing that header to select the URL, origin, proxy, redirect, or payload.

## Standards and current-tool rationale

Expand Down Expand Up @@ -72,6 +75,13 @@ be a complete `sha256` digest. Option lines, direct or local references, other
algorithms, truncated digests, and global directives are rejected even when they
contain a `--hash=` substring.

The download request sends a fixed repository-owned `User-Agent` value. This is
request metadata only: it does not change the literal URL, fixed HTTPS origin,
empty proxy map, redirect handler, byte bound, or archive digest. The explicit
value is required because the release host rejects Python's default
`urllib` identity with HTTP 403 even though the same fixed URL is available to
an explicit client identity.

The download request uses neither ambient proxy configuration nor automatic
redirect following. Any HTTP redirect is rejected before a request to the target
location can be created. The parsed response origin is still checked as defense
Expand Down Expand Up @@ -105,7 +115,8 @@ Regression coverage must prove:
- an absent sibling project is skipped, but an inventoried project blob that
cannot be read propagates a fatal error before uv starts;
- the download opener is cached, disables ambient proxies, and rejects redirects
before following them;
before following them; the download request uses the fixed repository-owned
`User-Agent`;
- fixed HTTPS scheme and hostname validation, acceptance only of an absent or
explicit port 443, rejection of malformed and nondefault ports, bounded reads,
archive digest, member type, member size, executable size, executable mode,
Expand Down Expand Up @@ -171,6 +182,9 @@ accepted by the coverage sandbox.

## References

Internet Engineering Task Force. (2022). *HTTP semantics* (RFC 9110),
Section 10.1.5, User-Agent. https://www.rfc-editor.org/rfc/rfc9110#section-10.1.5

Astral Software, Inc. (n.d.). *Exporting a lockfile*. uv documentation. Retrieved
August 4, 2026, from https://docs.astral.sh/uv/concepts/projects/export/

Expand Down
8 changes: 7 additions & 1 deletion scripts/ci/materialize_base_python_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
UV_SHA256_HASH_RE = re.compile(r"--hash=sha256:[0-9a-fA-F]{64}")
UV_EXPORT_TIMEOUT_SECONDS = 120
TRUSTED_UV_VERSION = "0.12.1"
# The release host rejects Python's default User-Agent with HTTP 403.
TRUSTED_UV_USER_AGENT = "ContextualWisdomLab-OpenCode-Coverage/1"
TRUSTED_UV_ARCHIVE_URL = (
"https://releases.astral.sh/github/uv/releases/download/0.12.1/"
"uv-x86_64-unknown-linux-gnu.tar.gz"
Expand Down Expand Up @@ -172,9 +174,13 @@ def _download_trusted_uv_archive() -> bytes:
# Keep the audited URL literal at the network sink so static analysis can
# prove that neither user data nor repository content selects a scheme,
# host, path, query, fragment, method, or request header.
with urllib.request.urlopen( # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310
request = urllib.request.Request(
"https://releases.astral.sh/github/uv/releases/download/0.12.1/"
"uv-x86_64-unknown-linux-gnu.tar.gz",
headers={"User-Agent": TRUSTED_UV_USER_AGENT},
)
with urllib.request.urlopen( # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310
request,
timeout=TRUSTED_UV_DOWNLOAD_TIMEOUT_SECONDS,
) as response:
final_url = urllib.parse.urlparse(response.geturl())
Expand Down
14 changes: 12 additions & 2 deletions tests/test_materialize_base_python_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,12 +503,22 @@ def _trusted_uv_archive(
def test_download_trusted_uv_archive_accepts_fixed_https_origin(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The downloader returns bounded bytes from the fixed Astral HTTPS origin."""
"""The downloader sends a stable User-Agent to the fixed Astral HTTPS origin."""
payload = b"archive"
response = FakeHttpResponse(materializer.TRUSTED_UV_ARCHIVE_URL, payload)
monkeypatch.setattr(materializer.urllib.request, "urlopen", lambda *_a, **_k: response)
captured: dict[str, object] = {}

def fake_urlopen(request: object, **_kwargs: object) -> FakeHttpResponse:
captured["request"] = request
return response

monkeypatch.setattr(materializer.urllib.request, "urlopen", fake_urlopen)

assert materializer._download_trusted_uv_archive() == payload
request = captured["request"]
assert isinstance(request, materializer.urllib.request.Request)
assert request.full_url == materializer.TRUSTED_UV_ARCHIVE_URL
assert request.get_header("User-agent") == materializer.TRUSTED_UV_USER_AGENT


def test_download_trusted_uv_archive_rejects_unsafe_redirect(
Expand Down
40 changes: 31 additions & 9 deletions tests/test_trusted_uv_download_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"https://releases.astral.sh/github/uv/releases/download/0.12.1/"
"uv-x86_64-unknown-linux-gnu.tar.gz"
)
_EXPECTED_USER_AGENT = "ContextualWisdomLab-OpenCode-Coverage/1"
_SEMGREP_DYNAMIC_URL_RULE = (
"python.lang.security.audit.dynamic-urllib-use-detected."
"dynamic-urllib-use-detected"
Expand Down Expand Up @@ -53,25 +54,29 @@ def _urlopen_calls() -> list[ast.Call]:
]


def test_urlopen_receives_one_literal_https_release_url() -> None:
"""Static analysis can prove repository or user data never selects the URL."""
def test_urlopen_receives_one_static_request() -> None:
"""Static analysis can prove the downloader passes one named request object."""
calls = _urlopen_calls()

assert len(calls) == 1
assert len(calls[0].args) == 1
url_argument = calls[0].args[0]
assert isinstance(url_argument, ast.Constant)
assert isinstance(url_argument.value, str)
assert url_argument.value == _EXPECTED_URL
request_argument = calls[0].args[0]
assert isinstance(request_argument, ast.Name)
assert request_argument.id == "request"


def test_literal_network_sink_matches_the_documented_release_constant() -> None:
"""The scanner-friendly sink literal cannot drift from the release identity."""
assert _assigned_literal("TRUSTED_UV_ARCHIVE_URL") == _EXPECTED_URL


def test_downloader_never_constructs_a_dynamic_request_object() -> None:
"""The audited downloader cannot hide a dynamic URL inside ``Request``."""
def test_literal_user_agent_matches_the_documented_identity() -> None:
"""The request identity remains fixed and contains no user-controlled data."""
assert _assigned_literal("TRUSTED_UV_USER_AGENT") == _EXPECTED_USER_AGENT


def test_downloader_constructs_one_static_request() -> None:
"""The request URL and User-Agent are both statically constrained."""
request_calls = [
node
for node in ast.walk(_download_function())
Expand All @@ -80,7 +85,24 @@ def test_downloader_never_constructs_a_dynamic_request_object() -> None:
and node.func.attr == "Request"
]

assert request_calls == []
assert len(request_calls) == 1
request_call = request_calls[0]
assert len(request_call.args) == 1
url_argument = request_call.args[0]
assert isinstance(url_argument, ast.Constant)
assert url_argument.value == _EXPECTED_URL

headers_keyword = next(
keyword for keyword in request_call.keywords if keyword.arg == "headers"
)
assert isinstance(headers_keyword.value, ast.Dict)
assert len(headers_keyword.value.keys) == 1
key = headers_keyword.value.keys[0]
value = headers_keyword.value.values[0]
assert isinstance(key, ast.Constant)
assert key.value == "User-Agent"
assert isinstance(value, ast.Name)
assert value.id == "TRUSTED_UV_USER_AGENT"


def test_literal_urlopen_sink_has_one_scoped_semgrep_suppression() -> None:
Expand Down
Loading