Skip to content

security: verify SHA256 of juliaup's own tarball before extraction#1456

Draft
IanButterworth wants to merge 8 commits intomainfrom
ib/security_verify_installs
Draft

security: verify SHA256 of juliaup's own tarball before extraction#1456
IanButterworth wants to merge 8 commits intomainfrom
ib/security_verify_installs

Conversation

@IanButterworth
Copy link
Copy Markdown
Member

On top of #1455

Adds SHA256 verification to the juliaup self-update and first-install paths.

Developed with Claude:


What

Before extracting the juliaup tarball during self-update or first install, fetch a SHA256SUMS file from the same server, look up the expected hash for the specific tarball, and bail if the digest of the downloaded bytes does not match. The archive is never written to disk if the check fails.

The release CI (package-unix) now generates the SHA256SUMS file immediately after creating the archives and uploads it to S3 alongside them.

Why

The previous flow trusted HTTPS alone:

fetch version string → pass semver check → download .tar.gz → unpack over exe dir

A compromise of the S3 bucket, CDN, or anything in between was sufficient to push arbitrary code to every user who ran juliaup self update or ran the installer. There was no cryptographic check between "bytes received from the network" and "bytes executed on disk".

What changes

  • release.yml (package-unix): add a Generate SHA256SUMS step that runs sha256sum juliaup-<VERSION>-*.tar.gz and writes the result to juliaup-<VERSION>-SHA256SUMS, which is uploaded to S3 with the rest.
  • src/operations.rs: two new #[cfg(not(windows))] functions:
    • download_sha256sums_entry — fetches and parses the SHA256SUMS file, returning the expected hex digest for a given filename.
    • download_extract_sans_parent_verified — buffers the full download, verifies the SHA-256 before touching disk, then calls unpack_sans_parent.
  • src/command_selfupdate.rs: use the verified path for self-update.
  • src/bin/juliainstaller.rs: use the verified path for first install.
  • Cargo.toml: add sha2 = "0.10" and hex = "0.4".

Scope

This only covers juliaup's own binary (self-update and first install). Julia version tarballs are not yet verified — those downloads still use download_extract_sans_parent directly and would require SHA256SUMS entries in the Julia release infrastructure to verify.

Limitations / follow-ups

  • The SHA256SUMS file itself is not signed. An attacker who controls the whole server can replace both the tarball and its hash. Signing the SHA256SUMS file (e.g. with a project Ed25519 key or Sigstore/cosign) is
    the next step and would close this remaining gap.
  • Windows self-update and install go through MSI / Windows Store mechanisms (which have OS-level signing) and are not affected by this change.
  • Custom JULIAUP_SERVER mirrors must host a matching juliaup-<VERSION>-SHA256SUMS file alongside the tarballs, otherwise self-update and first install will fail with an error.

Rollout

The first release built from this commit will generate and upload its own SHA256SUMS. Existing installations (built before this change) are unaffected until they self-update to that release; after that, all subsequent juliaup-binary updates are verified.

IanButterworth and others added 8 commits March 25, 2026 09:28
Previously, non-Normal path components (e.g. '..' or absolute roots)
were silently skipped, meaning a crafted archive could selectively
omit files while appearing to succeed. Abort immediately instead so
any unexpected archive structure is surfaced as an error.

Co-authored-by: Claude <claude@anthropic.com>
…nd log when set

Reject any non-HTTPS value supplied via these env vars so that a
misconfiguration or malicious override cannot silently downgrade
downloads to plain HTTP.

Also print a one-time informational message when a custom server URL
is in use, so users can see at a glance that downloads are not going
to the official julialang.org servers.

Co-authored-by: Claude <claude@anthropic.com>
- unpack_accepts_normal_paths: verify clean tarballs extract correctly
- unpack_rejects_path_traversal_dotdot: verify crafted archives with
  '..' components are rejected with an error
- juliaup_server_rejects_http / juliaup_nightly_server_rejects_http:
  verify non-HTTPS values for JULIAUP_SERVER / JULIAUP_NIGHTLY_SERVER
  are rejected
- juliaup_server_accepts_https: verify valid HTTPS override is accepted

Co-authored-by: Claude <claude@anthropic.com>
- unpack_rejects_path_traversal_dotdot: tar::Builder rejects '..' paths
  itself, so the test setup was failing before reaching unpack_sans_parent.
  Replace with a hand-rolled raw tar.gz that writes the path bytes directly
  into the header, bypassing the builder's check. The ParentDir component
  then reaches our own bail! as intended.

- unpack_accepts_normal_paths: entry.unpack() requires the parent directory
  to already exist. Add a directory entry for 'top/bin' before the file
  entry so the extraction succeeds.

Co-authored-by: Claude <claude@anthropic.com>
The tests asserted starts_with("1.10.10") but 1.10.11 has since been
released. Check that the channel was updated past 1.10.9 instead, which
is what the test semantically verifies.

These failures are unrelated to the security changes in this branch.

Co-authored-by: Claude <claude@anthropic.com>
The self-update and installer paths previously downloaded the juliaup
tarball and unpacked it directly with no integrity check. A server-side
compromise (CDN, S3 bucket) or MITM could substitute a malicious binary.

Changes:
- Release CI (package-unix): after creating all .tar.gz archives, run
  sha256sum to produce juliaup-<VERSION>-SHA256SUMS and upload it to S3
  alongside the tarballs.
- operations.rs: add download_sha256sums_entry (fetches and parses the
  SHA256SUMS text file) and download_extract_sans_parent_verified (buffers
  the download, verifies SHA-256 before touching disk, then extracts).
- command_selfupdate.rs: fetch SHA256SUMS before downloading the tarball
  and verify the hash before passing bytes to unpack_sans_parent.
- juliainstaller.rs: same.
- Cargo.toml: add sha2 = "0.10" and hex = "0.4".

This protects against accidental corruption and against scenarios where
only the binary is replaced but not the hash file (e.g. partial access to
the S3 bucket). Full protection against an attacker controlling the entire
distribution requires signing the SHA256SUMS file; that is a follow-up.

Co-authored-by: Claude <claude@anthropic.com>
@fingolfin
Copy link
Copy Markdown
Member

has conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants