Skip to content

brew-rs: default to anonymous GHCR bearer token in fetch#21889

Open
cachebag wants to merge 1 commit intoHomebrew:mainfrom
cachebag:main
Open

brew-rs: default to anonymous GHCR bearer token in fetch#21889
cachebag wants to merge 1 commit intoHomebrew:mainfrom
cachebag:main

Conversation

@cachebag
Copy link
Copy Markdown

@cachebag cachebag commented Apr 2, 2026


  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same change?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests (excluding integration tests) for your changes? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) with your changes locally?

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

I initally thought there was something going on with the redirect/Accept header interaction, but it just looks like the rust downloader depends on brew.sh setting HOMEBREW_GITHUB_PACKAGES_AUTH for GHCR bottle downloads. so when this variable isn't present (direct binary invocation, testing), GHCR returns 401 and the download fails silently.

this PR adds OCI bearer token negotiation as a fallback. essentially, when a GHCR blob request gets a 401 with a www-authenticate challenge, we now just fetch an anonymous bearer token from the token endpoint and retry. this falls in line with how standard OCI/Docker clients authenticate with public registries. falls back to Bearer QQ== when the env var is not set.

the existing HOMEBREW_GITHUB_PACKAGES_AUTH path is still of course, preferred when available.

Copy link
Copy Markdown
Member

@MikeMcQuaid MikeMcQuaid left a comment

Choose a reason for hiding this comment

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

Thanks!

@MikeMcQuaid MikeMcQuaid requested a review from Copilot April 2, 2026 16:07
@MikeMcQuaid
Copy link
Copy Markdown
Member

Holding off merge for Copilot.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves brew-rs bottle downloads from GHCR when HOMEBREW_GITHUB_PACKAGES_AUTH is not set by adding an OCI/Docker-style Bearer token negotiation fallback, enabling anonymous access to public GHCR blobs during direct binary invocation and tests.

Changes:

  • Refactors HTTP auth header resolution into resolve_http_auth.
  • Adds GHCR OCI blob detection and WWW-Authenticate Bearer challenge parsing.
  • Introduces token negotiation logic to obtain an anonymous bearer token and use it for downloads, plus unit tests for the parsing/detection helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +576 to +584
fn negotiate_oci_token(client: &Client, url: &Url) -> BrewResult<Option<String>> {
let probe = client
.head(url.as_str())
.send()
.with_context(|| format!("Failed to probe {url}"))?;

if probe.status() != reqwest::StatusCode::UNAUTHORIZED {
return Ok(None);
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

negotiate_oci_token always performs a separate HEAD probe before the actual GET, which adds an extra round trip per GHCR blob download (and will also fail to negotiate if the registry/proxy doesn’t support HEAD). Consider attempting the GET first and, only if it returns 401 with a WWW-Authenticate Bearer challenge, fetching the token and retrying the GET with Authorization: Bearer .... This avoids doubling requests on the common path and is more robust than relying on HEAD behavior.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

not sure i understand this...i'm pretty sure a HEAD is cheaper than starting a full GET of a bottle blob just to get a 401 and throw the body away

@MikeMcQuaid ?

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.

Ideally we avoid this extra HEAD. Goal here is to just emulate the Ruby code here as closely as possible.

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.

If the Ruby code does a HEAD: fine, if not, let's just copy it. If a GET gives you a 401 the content size will be tiny.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yep I just have it fall back to Bearer QQ== like brew.sh does.

Comment on lines +623 to +628
fn parse_bearer_challenge(header: &str) -> Option<BearerChallenge> {
let params = header.strip_prefix("Bearer ")?;
let realm = extract_challenge_param(params, "realm")?;
let service = extract_challenge_param(params, "service")?;
let scope = extract_challenge_param(params, "scope")?;
Some(BearerChallenge {
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

parse_bearer_challenge matches only the exact prefix "Bearer ". HTTP auth schemes are case-insensitive, so a server sending bearer/BEARER would cause negotiation to be skipped and the download to fail. Consider parsing the scheme in a case-insensitive way (e.g., split once on whitespace and compare with eq_ignore_ascii_case("bearer")).

Copilot uses AI. Check for mistakes.
Comment on lines +556 to +557
if should_send_github_packages_auth(url) {
if let Some(auth) = env_value("HOMEBREW_GITHUB_PACKAGES_AUTH") {
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

In resolve_http_auth, the outer if should_send_github_packages_auth(url) guard is redundant because should_send_github_packages_auth already depends on HOMEBREW_GITHUB_PACKAGES_AUTH being present, and then the code checks that env var again. This can be simplified to reduce duplicated env lookups and make the control flow clearer (e.g., check env_value("HOMEBREW_GITHUB_PACKAGES_AUTH") first, then apply the host/other gating logic).

Suggested change
if should_send_github_packages_auth(url) {
if let Some(auth) = env_value("HOMEBREW_GITHUB_PACKAGES_AUTH") {
if let Some(auth) = env_value("HOMEBREW_GITHUB_PACKAGES_AUTH") {
if should_send_github_packages_auth(url) {

Copilot uses AI. Check for mistakes.
@cachebag cachebag force-pushed the main branch 2 times, most recently from 3256cd6 to 8458c29 Compare April 2, 2026 16:35
@cachebag cachebag changed the title brew-rs: add OCI bearer token negotiation for GHCR downloads brew-rs: default to anonymous GHCR bearer token in fetch Apr 2, 2026
@cachebag cachebag requested a review from MikeMcQuaid April 2, 2026 16:52
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.

3 participants