# git-remote-entire: gate ENTIRE_TOKEN aud against cluster-trusted cores

`3831b79`·

toothbrush·1mo ago·5 files·+284 added/-18 removed

Adversarial review flagged that the ENTIRE_TOKEN path turned the token's unverified aud claim directly into the STS exchange host (SSRF / token exfiltration) and permitted cleartext http exchange.

Fixes:
- CoreURLFromEnvToken now enforces a strict origin: https scheme, host present, no userinfo/path/query/fragment. http (cleartext) and richer URLs are hard errors.
- Before exchanging, verify the aud is one of the cores the target cluster advertises at /.well-known/entire-cluster.json (fetched over TLS, keyed on the clone-URL host the user typed, reusing clusterdiscovery). A forged aud can no longer redirect the token to an attacker host; it aborts.
- Export clusterdiscovery.ResolveClusterCores for the cores-only lookup.

Trust model: claims are used only as a routing hint, constrained to TLS-vouched trusted cores; the core's STS remains the authoritative signature verifier. No client-side JWKS verification (matches the non-env login-context path).

Tests: strict aud validation (http/path/query/fragment/userinfo/opaque), plus the integrated gate against a fake well-known TLS server (trusted->ok, untrusted->abort, discovery failure->abort).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

72c794f4cb83View transcript

### ENTIRE_TOKEN Environment Override for CI

### Changes

5

- cmd
- entire/cli/auth
 - Menv_token.go+47/-7
 - Menv_token_test.go+33/-3

- git-remote-entire
 - Mmain.go+44/-8
 - Mmain_test.go+146

- internal/entireclient/clusterdiscovery
 - Mresolve.go+14

```
    19 unmodified lines
    ```

"// which is what STS routing keys on — so we read aud, not iss (iss may be a regional core that can't mint the cross-region exchange).
"// The aud claim is URL-shaped (scheme://host[/path]). It may be a single string or an array (RFC 7519 §4.1.3); ParseClaims normalises both to a slice. The first URL-shaped audience wins. A token with no URL-shaped aud is rejected with a clear error rather than silently falling back to context resolution, so a misconfigured CI token fails loudly.
"// SECURITY: the returned URL becomes the host the env token is POSTed to as a subject_token during exchange. ParseClaims does NOT verify the signature, so the audience is attacker-controlled if a forged token is injected. This function only enforces the *shape* of a safe endpoint (https, bare origin); the caller MUST additionally verify the URL is a trusted core for the target cluster (see clusterdiscovery.ResolveClusterCores) before exchanging, or a forged aud could redirect the token to an arbitrary host.

"// Structural rules, all required:
//   - the aud is a well-formed absolute URL,
//   - scheme is https (no cleartext token exchange),
//   - it carries a host and no userinfo, path, query, or fragment — entire cores are bare origins (https://core.example.com), so anything richer is either a misconfigured token or an attempt to smuggle a path/redirect.
 
"// The aud claim may be a single string or an array (RFC 7519 §4.1.3);

// ParseClaims normalises both to a slice. Non-URL audiences (e.g. an OAuth client_id like "entire-cli") are skipped; the first URL-shaped audience is validated strictly. A token with no URL-shaped aud is rejected with a clear error rather than silently falling back to context resolution.

```
func CoreURLFromEnvToken(rawToken string) (string, error) {
    claims, err := tokens.ParseClaims(rawToken)
    if err != nil {
        return "", fmt.Errorf("parse %s claims: %w", EnvTokenVar, err)
    }
    for _, aud := range claims.Audience {
        if u, err := url.Parse(aud); err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" {
            return strings.TrimRight(aud, "/"), nil
        }
u, perr := url.Parse(aud)
        if perr != nil || u.Scheme == "" {
            continue
        }
        return validateCoreAudience(u)
    }
    return "", fmt.Errorf("%s must be a login or sa-session JWT whose aud is the home-region URL; found no URL-shaped audience claim", EnvTokenVar)
}
    ```

```go
// validateCoreAudience enforces that u is a safe entire-core origin and returns its canonical form (scheme://host, no trailing slash).
func validateCoreAudience(u *url.URL) (string, error) {
switch {
case u.Scheme != "https":
return "", fmt.Errorf("%s aud %q must use https; refusing to exchange the token over %s", EnvTokenVar, u.String(), u.Scheme)
case u.Host == "":
return "", fmt.Errorf("%s aud %q has no host", EnvTokenVar, u.String())
case u.User != nil:
return "", fmt.Errorf("%s aud %q must not contain userinfo", EnvTokenVar, u.String())
case u.Path != "" && u.Path != "/":
return "", fmt.Errorf("%s aud %q must be a bare origin with no path", EnvTokenVar, u.String())
case u.RawQuery != "":
return "", fmt.Errorf("%s aud %q must not contain query parameters", EnvTokenVar, u.String())
case u.Fragment != "":
return "", fmt.Errorf("%s aud %q must not contain a fragment", EnvTokenVar, u.String())
}
return strings.TrimRight(u.Scheme + "://" + u.Host, "/"), nil
}
```

---
