Skip to content

feat: fix host header issue and ground work (not enabled) for signature verification in future PR#17

Merged
jhaynie merged 5 commits intomainfrom
task/fix-proxy-http-signature-signing
Mar 30, 2026
Merged

feat: fix host header issue and ground work (not enabled) for signature verification in future PR#17
jhaynie merged 5 commits intomainfrom
task/fix-proxy-http-signature-signing

Conversation

@jhaynie
Copy link
Copy Markdown
Member

@jhaynie jhaynie commented Mar 30, 2026

Summary

  • Fixes host header issue with vite
  • Wires the ion signing public key through the gravity handshake for future signature verification
  • Bumps go-common to v1.0.181

Problem

The ion signing transport removes Content-Length and sets it to -1 to enable streaming signature trailers over HTTP/2. When gravity's reverse proxy forwards these requests to the local agent over HTTP/1.1, the missing Content-Length causes chunked transfer encoding. Many dev servers (Vite, Node.js, etc.) cannot parse the chunked body, resulting in 400 Bad Request errors.

Solution

Buffer the request body in the handler and restore Content-Length before forwarding:

if r.ContentLength < 0 && r.Body != nil && r.Body != http.NoBody {
    bodyBytes, err = io.ReadAll(r.Body)
    r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
    r.ContentLength = int64(len(bodyBytes))
}

Signature Verification (TODO)

The plumbing for upstream signature verification is in place:

  • cmd/start.go parses the signing public key PEM from Configuration
  • StartServer accepts the signing key parameter
  • verifyUpstreamSignature is refactored to accept pre-buffered body bytes (never touches r.Body)

Verification is disabled pending:

  1. All ion instances consistently deriving the same HKDF signing key
  2. Investigation of POST body hash mismatches between ion signing and gravity verification

Files Changed

File Change
internal/stack/stack.go Buffer body + restore Content-Length; refactor verifyUpstreamSignature to not touch r.Body; verification disabled with TODO
cmd/start.go Parse SigningPublicKey PEM from Configuration, pass to StartServer
go.mod / go.sum go-common v1.0.181

Summary by CodeRabbit

  • New Features

    • Optional upstream ECDSA signing key support: startup logs indicate when a key is used or skipped; requests are verified when a valid key is provided.
  • Bug Fixes

    • Improved reverse-proxy request handling to preserve request bodies/trailers and maintain response logging for more reliable forwarding.
  • Chores

    • Updated dependency to a newer stable release.

jhaynie added 2 commits March 29, 2026 20:01
…ecation

- Replace deprecated proxy.Director with Rewrite (httputil.ReverseProxy)
- Add verifyUpstreamSignature() to validate incoming requests were signed
  by the Agentuity ion proxy using go-common/crypto HTTP signature headers
- Signature is read from HTTP trailer; body is buffered and restored for
  downstream proxying
- Verification currently logs warnings only (non-enforcing) pending the
  ion-side change to send its signing public key via provider.Configuration
… signing key

The ion signing transport removes Content-Length to enable streaming
signature trailers over HTTP/2. When the gravity dev-mode proxy forwards
to the local agent over HTTP/1.1, the missing Content-Length causes
chunked transfer encoding which many dev servers cannot parse (400).

Buffer the body and restore Content-Length before forwarding. Also wire
the signing public key from ion through the Configuration so signature
verification is ready to enable once all ion instances consistently
derive the same HKDF signing key.

Changes:
- stack.go: buffer body when Content-Length is -1, restore Content-Length
- stack.go: refactor verifyUpstreamSignature to accept pre-buffered body
  bytes without touching r.Body (verification disabled via TODO)
- cmd/start.go: parse SigningPublicKey PEM from Configuration, pass to
  StartServer
- go.mod: bump go-common to v1.0.181
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Parses an optional upstream ECDSA public key at startup and passes it into StartServer; refactors the reverse proxy to use httputil.ReverseProxy with Rewrite and response modification; adds verifyUpstreamSignature helper and a one-time warning when upstream key is absent but agent has a private key.

Changes

Cohort / File(s) Summary
Dependency Update
go.mod
Bumped github.com/agentuity/go-common from v1.0.165 to v1.0.181.
Startup: parse and pass signing key
cmd/start.go
Parses PEM PKIX ECDSA public key from c.SigningPublicKey; PEM-decodes, PKIX-parses, and type-asserts to *ecdsa.PublicKey; logs warnings on failures and Info on success; passes (agent, signingKey) into stack.StartServer.
Proxy & verification logic
internal/stack/stack.go
Updated StartServer signature to accept agent AgentMetadata, signingKey *ecdsa.PublicKey. Replaced httputil.NewSingleHostReverseProxy with httputil.ReverseProxy using Rewrite, FlushInterval, ErrorHandler, and ModifyResponse. Added verifyUpstreamSignature(logger, publicKey, r) error to buffer/read request bodies, extract signature headers/trailers, verify via agcrypto, and restore r.Body/r.ContentLength. Emits a one-time warning if signingKey is nil while agent.PrivateKey is non-nil.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/stack/stack.go (2)

172-175: Comment says "log once" but this logs on every StartServer call.

The warning fires each time StartServer is invoked (e.g., on reconnection). If the intent is truly once-per-process, consider a sync.Once or package-level flag. Otherwise, updating the comment to clarify "once per connection" would be more accurate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/stack/stack.go` around lines 172 - 175, The comment claiming "Log
once" is misleading because the logger.Warn call in StartServer (the block
checking signingKey == nil && agent.PrivateKey != nil) runs on every StartServer
invocation; either make the behavior truly once-per-process by adding a
package-level sync.Once or boolean guard and invoke logger.Warn inside that
guard (referencing signingKey, agent.PrivateKey and StartServer), or update the
comment to accurately state it logs once per StartServer/connection instead of
once per process; choose one approach and apply consistently.

215-221: Track this TODO for enabling signature verification.

The explanation is clear and well-documented. Once ion-side key consistency is resolved, this verification should be enabled.

Would you like me to open an issue to track enabling upstream signature verification once the ion-side prerequisites are met?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/stack/stack.go` around lines 215 - 221, Create a tracked issue to
re-enable upstream signature verification referenced by the TODO in stack.go and
the verifyUpstreamSignature() function: describe the prerequisite that all ion
instances must derive the same HKDF signing key (include how to detect key
divergence), list concrete steps to enable verification (ensure consistent HKDF
key derivation, update rolling-deploy procedure, re-enable
verifyUpstreamSignature() call, and restore POST body hash checks), add required
tests (unit/integration for signature verification and a rolling-deploy
integration test), and note rollout criteria and rollback plan for deployments
where keys may differ; link the issue to the stack.go TODO and any relevant
CI/deployment docs for follow-up.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/start.go`:
- Around line 150-164: The current block around c.SigningPublicKey decodes and
parses the PEM but treats a non-ECDSA parsed key the same as a parse error,
causing a misleading "failed to parse it" warning; change the logic in the
signing key handling (the pem.Decode / x509.ParsePKIXPublicKey path that assigns
signingKey via pub.(*ecdsa.PublicKey)) to differentiate parse failure from type
mismatch: if x509.ParsePKIXPublicKey returns an error log that parse error, but
if parse succeeds and the type assertion to *ecdsa.PublicKey yields nil, log a
clear warning that the key parsed but is the wrong type and include the actual
parsed key type (e.g., via reflect.TypeOf(pub) or fmt.Sprintf("%T", pub));
retain the existing logger.Info when signingKey is set.

In `@internal/stack/stack.go`:
- Around line 202-213: The current use of io.ReadAll on r.Body in the request
handling block (checking r.ContentLength, r.Body and http.NoBody) can OOM;
replace it with a bounded read using io.LimitReader (or similar) with a defined
maxBufferSize constant, read up to that limit, detect if the body exceeded the
limit and return a 413 or 502 error and log via logger.Error; if within limit,
recreate r.Body with bytes.NewReader of the bytes read and set r.ContentLength
to the actual length. Ensure symbols referenced are r.Body, r.ContentLength,
io.ReadAll (replace), io.LimitReader, bytes.NewReader, and logger.Error.

---

Nitpick comments:
In `@internal/stack/stack.go`:
- Around line 172-175: The comment claiming "Log once" is misleading because the
logger.Warn call in StartServer (the block checking signingKey == nil &&
agent.PrivateKey != nil) runs on every StartServer invocation; either make the
behavior truly once-per-process by adding a package-level sync.Once or boolean
guard and invoke logger.Warn inside that guard (referencing signingKey,
agent.PrivateKey and StartServer), or update the comment to accurately state it
logs once per StartServer/connection instead of once per process; choose one
approach and apply consistently.
- Around line 215-221: Create a tracked issue to re-enable upstream signature
verification referenced by the TODO in stack.go and the
verifyUpstreamSignature() function: describe the prerequisite that all ion
instances must derive the same HKDF signing key (include how to detect key
divergence), list concrete steps to enable verification (ensure consistent HKDF
key derivation, update rolling-deploy procedure, re-enable
verifyUpstreamSignature() call, and restore POST body hash checks), add required
tests (unit/integration for signature verification and a rolling-deploy
integration test), and note rollout criteria and rollback plan for deployments
where keys may differ; link the issue to the stack.go TODO and any relevant
CI/deployment docs for follow-up.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2696de45-3c7e-4c7d-84f3-43a1a706de71

📥 Commits

Reviewing files that changed from the base of the PR and between 10afe65 and 0d230c2.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • cmd/start.go
  • go.mod
  • internal/stack/stack.go
📜 Review details
🔇 Additional comments (4)
go.mod (1)

6-6: Dependency bump matches the PR intent.

Updating github.com/agentuity/go-common to v1.0.181 is consistent with the new signing-key and proxy-bridge behavior introduced in this PR.

internal/stack/stack.go (2)

145-170: LGTM on the proxy configuration.

The switch from the deprecated Director to Rewrite is correct. The X-Forwarded-Host restoration ensures the local dev server sees the expected public hostname, and the ErrorHandler properly suppresses expected context cancellation noise.


254-293: LGTM on the signature verification helper.

The function is well-structured: it accepts pre-buffered body bytes to avoid modifying r.Body, handles both trailer and header signature sources, and provides useful debug logging. Ready for enablement once the ion-side prerequisites are resolved.

cmd/start.go (1)

165-165: LGTM on the updated StartServer invocation.

The call correctly passes the agent metadata and parsed signing key, matching the updated function signature.

Comment thread cmd/start.go
Comment thread internal/stack/stack.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
cmd/start.go (1)

159-161: Avoid logging this as active verification yet.

The PR still leaves upstream signature verification disabled, so Line 161 reads as if the proxy is already enforcing it. Consider logging that the key was loaded instead, and reserve the verification wording for when the check is actually enabled.

💡 Possible wording
-					logger.Info("using upstream signing key for proxy verification")
+					logger.Info("loaded upstream signing key")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/start.go` around lines 159 - 161, The log message currently says
verification is active though it only assigns signingKey = ecKey; change the log
emitted from logger.Info to state that the upstream signing key was loaded or
available (e.g., "loaded upstream signing key for potential verification") so it
doesn't imply enforcement; keep the assignment to signingKey and only log that
verification is enabled at the actual verification-enabling code path where the
verification flag/logic runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/start.go`:
- Around line 159-161: The log message currently says verification is active
though it only assigns signingKey = ecKey; change the log emitted from
logger.Info to state that the upstream signing key was loaded or available
(e.g., "loaded upstream signing key for potential verification") so it doesn't
imply enforcement; keep the assignment to signingKey and only log that
verification is enabled at the actual verification-enabling code path where the
verification flag/logic runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25448f5b-7a7e-4558-ab99-ccc56bd22147

📥 Commits

Reviewing files that changed from the base of the PR and between 0d230c2 and 0989fbb.

📒 Files selected for processing (1)
  • cmd/start.go

jhaynie added 2 commits March 30, 2026 00:27
Ion PR #380 preserves Content-Length through the signing transport,
so gravity no longer needs to buffer the request body in RAM to
restore it. Requests pass straight through to the reverse proxy.
The plumbing is in place (signing key parsing, verifyUpstreamSignature
function, StartServer parameter) but verification is disabled pending
investigation of keyid inconsistencies across ion instances.

The Content-Length fix (ion PR #380) is the ship-blocking change —
it resolves the 400 Bad Request on POST endpoints.
@jhaynie jhaynie changed the title feat: buffer request body for HTTP/2→HTTP/1.1 proxy bridging and wire signing key feat: fix host header issue and ground work (not enabled) for signature verification in future PR Mar 30, 2026
@jhaynie jhaynie merged commit 53aec3b into main Mar 30, 2026
1 check was pending
@jhaynie jhaynie deleted the task/fix-proxy-http-signature-signing branch March 30, 2026 07:44
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/stack/stack.go (1)

182-197: ⚠️ Potential issue | 🔴 Critical

Wire up the signature verification to restore Content-Length before proxying.

The verifyUpstreamSignature function exists to read the body, save the original Content-Length, verify the signature, and restore both—preventing chunked encoding to localhost. However, it is never called in the request handler. Non-health requests go directly to proxy.ServeHTTP(w, r) without any normalization, so requests that arrive without Content-Length are still forwarded to localhost as chunked and will hit the same 400s this PR is trying to eliminate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/stack/stack.go` around lines 182 - 197, The handler currently
forwards non-/_health requests directly to proxy.ServeHTTP without calling
verifyUpstreamSignature, so bodies read for signature verification and the
original Content-Length restoration never happen; update the request handling in
the HTTP handler (the anonymous http.HandlerFunc) to call
verifyUpstreamSignature(w, r) before proxy.ServeHTTP and only proxy when
verifyUpstreamSignature succeeds (restores Content-Length and body), ensuring
verifyUpstreamSignature is invoked for non-health paths and its restored request
is passed into proxy.ServeHTTP instead of forwarding the original chunked
request.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/stack/stack.go`:
- Around line 246-278: The Content-Length restoration and trailer/header cleanup
around signature verification are incorrect: if origContentLength is -1 (common
for HTTP/2) you must set r.ContentLength to the normalized length
int64(len(body)) instead of restoring -1, and you must remove the signature from
the request before forwarding (strip agcrypto.HeaderSignature from r.Trailer
when the signature source is "trailer" and from r.Header when source is
"header"). Locate the block using origContentLength, body, sig, source,
agcrypto.HeaderSignature and VerifyHTTPRequest and change the post-read logic to
(1) delete the signature from the appropriate place based on source, (2) restore
r.Body to io.NopCloser(bytes.NewReader(body)), and (3) set r.ContentLength =
origContentLength if origContentLength >= 0 else r.ContentLength =
int64(len(body)).

---

Outside diff comments:
In `@internal/stack/stack.go`:
- Around line 182-197: The handler currently forwards non-/_health requests
directly to proxy.ServeHTTP without calling verifyUpstreamSignature, so bodies
read for signature verification and the original Content-Length restoration
never happen; update the request handling in the HTTP handler (the anonymous
http.HandlerFunc) to call verifyUpstreamSignature(w, r) before proxy.ServeHTTP
and only proxy when verifyUpstreamSignature succeeds (restores Content-Length
and body), ensuring verifyUpstreamSignature is invoked for non-health paths and
its restored request is passed into proxy.ServeHTTP instead of forwarding the
original chunked request.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d560d77b-93ea-47a0-b3dd-0600b0b60d47

📥 Commits

Reviewing files that changed from the base of the PR and between bdbbcb9 and a9e526c.

📒 Files selected for processing (1)
  • internal/stack/stack.go
📜 Review details
🔇 Additional comments (1)
internal/stack/stack.go (1)

172-175: Good startup-only warning.

This surfaces the missing upstream key without turning every request into noise.

Comment thread internal/stack/stack.go
Comment on lines +246 to +278
// Read the full body so HTTP trailers become available. Save the
// original Content-Length so we can restore it after verification —
// the reverse proxy needs it to avoid chunked encoding to localhost.
origContentLength := r.ContentLength
body, err := io.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("read body: %w", err)
}

// The ion proxy sends the Signature as an HTTP trailer (for streaming
// requests) or as a header (for WebSocket requests).
sig := r.Trailer.Get(agcrypto.HeaderSignature)
source := "trailer"
if sig == "" {
sig = r.Header.Get(agcrypto.HeaderSignature)
source = "header"
if sig == "" {
source = "missing"
}
}
logger.Debug("upstream signature value: source=%s present=%v", source, sig != "")

// Verify the cryptographic signature against the upstream signing key.
verifyErr := agcrypto.VerifyHTTPRequest(publicKey, r, body, nil)
if verifyErr != nil {
logger.Debug("upstream signature verification failed: %v", verifyErr)
} else {
logger.Debug("upstream signature verification succeeded")
}

// Restore the body and Content-Length so the reverse proxy can forward it.
r.Body = io.NopCloser(bytes.NewReader(body))
r.ContentLength = origContentLength
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd internal/stack && sed -n '240,285p' stack.go

Repository: agentuity/gravity

Length of output: 1568


🏁 Script executed:

# Let's also check the full function context
cd internal/stack && wc -l stack.go

Repository: agentuity/gravity

Length of output: 73


🏁 Script executed:

# Find the function name and broader context
cd internal/stack && grep -n "verifyUpstreamSignature\|func.*signature" stack.go | head -20

Repository: agentuity/gravity

Length of output: 251


🏁 Script executed:

# Check if there are tests or related usage
rg -n "verifyUpstreamSignature|origContentLength" internal/

Repository: agentuity/gravity

Length of output: 430


🏁 Script executed:

# Find where verifyUpstreamSignature is called
rg -n "verifyUpstreamSignature" internal/

Repository: agentuity/gravity

Length of output: 299


🏁 Script executed:

# Check for HTTP/2 references in the codebase
rg -n "HTTP/2|h2|:authority|pseudo-header" internal/stack/ | head -20

Repository: agentuity/gravity

Length of output: 136


🏁 Script executed:

# Look at the function signature and its callers more broadly
cd internal/stack && sed -n '229,285p' stack.go

Repository: agentuity/gravity

Length of output: 2232


🏁 Script executed:

# Check if TransferEncoding is manipulated anywhere in this file
rg -n "TransferEncoding" internal/stack/

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

# Look for calls to verifyUpstreamSignature
rg -n "verifyUpstreamSignature\(" internal/

Repository: agentuity/gravity

Length of output: 193


🏁 Script executed:

# Check the broader stack.go file to see request handling flow
cd internal/stack && sed -n '1,50p' stack.go

Repository: agentuity/gravity

Length of output: 1039


🏁 Script executed:

# Search for how requests flow through this code
rg -n "func.*Request\|http\.Request" internal/stack/stack.go | head -10

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

# Search all files for calls to verifyUpstreamSignature
rg "verifyUpstreamSignature" --type go

Repository: agentuity/gravity

Length of output: 291


🏁 Script executed:

# Look for HTTP handler or middleware that might use this
rg -n "ServeHTTP\|Handler\|Middleware" internal/stack/stack.go | head -20

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

# Check if there's any proxy configuration or request handling
rg -n "ReverseProxy\|Director\|Transport" internal/stack/stack.go | head -20

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

# Search all go files in the repo for calls
rg "verifyUpstreamSignature\(" . --type go

Repository: agentuity/gravity

Length of output: 191


🏁 Script executed:

# Check if the function is dead code or commented out
rg -B5 -A5 "verifyUpstreamSignature\(" internal/stack/stack.go

Repository: agentuity/gravity

Length of output: 643


🏁 Script executed:

# Look for HTTP/2 streaming or trailer handling
rg -n "Trailer\|trailer" internal/stack/stack.go

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

# Check the context after the function - what calls it or how it integrates
cd internal/stack && sed -n '278,350p' stack.go

Repository: agentuity/gravity

Length of output: 2080


🏁 Script executed:

# Search more broadly - maybe it's commented out or in a different pattern
rg "verifyUpstream" . --type go

Repository: agentuity/gravity

Length of output: 295


🏁 Script executed:

# Check if there are any TODOs or comments about enabling this
rg -n "TODO\|FIXME\|disabled\|commented" internal/stack/stack.go | head -20

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

# Look at the imports to understand the context better
cd internal/stack && head -40 stack.go

Repository: agentuity/gravity

Length of output: 858


🏁 Script executed:

# Check if there are other signature verification patterns
rg -n "VerifyHTTP\|signature" internal/stack/stack.go

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

# Search for any references or comments about this being disabled/unused
rg -B10 "func verifyUpstreamSignature" internal/stack/stack.go

Repository: agentuity/gravity

Length of output: 464


🏁 Script executed:

# Check if there's a git history that might explain the function
git log --oneline -n 50 -- internal/stack/stack.go 2>/dev/null | head -20

Repository: agentuity/gravity

Length of output: 114


🏁 Script executed:

# Look for any test files that might reference this function
find . -name "*test*.go" -type f | xargs grep -l "verifyUpstreamSignature" 2>/dev/null

Repository: agentuity/gravity

Length of output: 45


🏁 Script executed:

# Check the full file for context on where this function sits
cd internal/stack && wc -l stack.go && grep -n "^func " stack.go

Repository: agentuity/gravity

Length of output: 792


🏁 Script executed:

# Check the git commit that disabled signature verification
git show a9e526c --stat 2>/dev/null | head -30

Repository: agentuity/gravity

Length of output: 1438


🏁 Script executed:

# Look for any calls to verifyUpstreamSignature in the actual code flow
rg -n "verifyUpstream" . --type go -A 2 -B 2

Repository: agentuity/gravity

Length of output: 798


🏁 Script executed:

# Check StartServer function to see if it calls verifyUpstreamSignature
cd internal/stack && sed -n '136,232p' stack.go

Repository: agentuity/gravity

Length of output: 3681


🏁 Script executed:

# Check if there's documentation or comments about HTTP/2 handling
rg -n "HTTP/2\|streaming\|chunked\|TransferEncoding" . --type go

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

# Look at the agcrypto module to understand VerifyHTTPRequest
rg -n "VerifyHTTPRequest" . --type go

Repository: agentuity/gravity

Length of output: 156


🏁 Script executed:

# Check Go's net/http documentation behavior - let's verify ContentLength in HTTP/2
python3 << 'EOF'
# HTTP/2 spec: Content-Length header is optional
# In Go's net/http package, when a request body is read via io.ReadAll
# and the request came from HTTP/2, ContentLength may be -1 (unknown)
# This is well-documented in Go's http.Request struct comments

# Let's verify our understanding about the fix
print("HTTP/2 ContentLength behavior:")
print("- HTTP/2 doesn't require Content-Length header")
print("- Go's http.Request sets ContentLength = -1 when reading streamed/chunked bodies")
print("- After ReadAll(), we know the exact length, so setting ContentLength = len(body) is correct")
print("- Clearing TransferEncoding prevents chunked encoding downstream")
EOF

Repository: agentuity/gravity

Length of output: 377


🏁 Script executed:

# Check if there are any other places in the codebase that handle Content-Length restoration
rg -n "ContentLength\|Transfer-Encoding" . --type go -A 2 -B 2

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

# Let's verify the HTTP module behavior and understand if our analysis is sound
# Check the exact behavior of http.Request.ContentLength
python3 << 'EOF'
# Based on Go's documentation:
# - http.Request.ContentLength is set to -1 for requests without Content-Length header
# - This happens with chunked encoding, streaming, or HTTP/2
# - After io.ReadAll(r.Body), we know the exact length
# - Setting ContentLength to the actual length is the correct fix
# - TransferEncoding should be cleared to prevent proxy from re-chunking

facts = [
    "http.Request has ContentLength int64 field",
    "ContentLength = -1 means 'unknown length' (chunked/streamed)",
    "http.Request.TransferEncoding []string contains encoding names",
    "After ReadAll(), body size is known, should set ContentLength = len(body)",
    "TransferEncoding should be cleared to prevent downstream chunking"
]

for fact in facts:
    print(f"✓ {fact}")
EOF

Repository: agentuity/gravity

Length of output: 377


Fix the Content-Length restoration before re-enabling signature verification.

This function is currently disabled (commit a9e526c), but when signature verification is re-enabled, the Content-Length restoration logic needs correction. For HTTP/2 requests, origContentLength is often -1. Restoring that value after ReadAll() reintroduces an unknown-length body, which defeats the normalization and allows the reverse proxy to fall back to chunked transfer encoding. Additionally, if the signature came via HTTP trailer, that must be stripped from the forwarded request.

💡 Required fix for re-enablement
-	// Read the full body so HTTP trailers become available. Save the
-	// original Content-Length so we can restore it after verification —
-	// the reverse proxy needs it to avoid chunked encoding to localhost.
-	origContentLength := r.ContentLength
+	// Read the full body so HTTP trailers become available.
 	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		return fmt.Errorf("read body: %w", err)
 	}
@@
 	// Restore the body and Content-Length so the reverse proxy can forward it.
 	r.Body = io.NopCloser(bytes.NewReader(body))
-	r.ContentLength = origContentLength
+	r.ContentLength = int64(len(body))
+	r.TransferEncoding = nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Read the full body so HTTP trailers become available. Save the
// original Content-Length so we can restore it after verification —
// the reverse proxy needs it to avoid chunked encoding to localhost.
origContentLength := r.ContentLength
body, err := io.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("read body: %w", err)
}
// The ion proxy sends the Signature as an HTTP trailer (for streaming
// requests) or as a header (for WebSocket requests).
sig := r.Trailer.Get(agcrypto.HeaderSignature)
source := "trailer"
if sig == "" {
sig = r.Header.Get(agcrypto.HeaderSignature)
source = "header"
if sig == "" {
source = "missing"
}
}
logger.Debug("upstream signature value: source=%s present=%v", source, sig != "")
// Verify the cryptographic signature against the upstream signing key.
verifyErr := agcrypto.VerifyHTTPRequest(publicKey, r, body, nil)
if verifyErr != nil {
logger.Debug("upstream signature verification failed: %v", verifyErr)
} else {
logger.Debug("upstream signature verification succeeded")
}
// Restore the body and Content-Length so the reverse proxy can forward it.
r.Body = io.NopCloser(bytes.NewReader(body))
r.ContentLength = origContentLength
// Read the full body so HTTP trailers become available.
body, err := io.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("read body: %w", err)
}
// The ion proxy sends the Signature as an HTTP trailer (for streaming
// requests) or as a header (for WebSocket requests).
sig := r.Trailer.Get(agcrypto.HeaderSignature)
source := "trailer"
if sig == "" {
sig = r.Header.Get(agcrypto.HeaderSignature)
source = "header"
if sig == "" {
source = "missing"
}
}
logger.Debug("upstream signature value: source=%s present=%v", source, sig != "")
// Verify the cryptographic signature against the upstream signing key.
verifyErr := agcrypto.VerifyHTTPRequest(publicKey, r, body, nil)
if verifyErr != nil {
logger.Debug("upstream signature verification failed: %v", verifyErr)
} else {
logger.Debug("upstream signature verification succeeded")
}
// Restore the body and Content-Length so the reverse proxy can forward it.
r.Body = io.NopCloser(bytes.NewReader(body))
r.ContentLength = int64(len(body))
r.TransferEncoding = nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/stack/stack.go` around lines 246 - 278, The Content-Length
restoration and trailer/header cleanup around signature verification are
incorrect: if origContentLength is -1 (common for HTTP/2) you must set
r.ContentLength to the normalized length int64(len(body)) instead of restoring
-1, and you must remove the signature from the request before forwarding (strip
agcrypto.HeaderSignature from r.Trailer when the signature source is "trailer"
and from r.Header when source is "header"). Locate the block using
origContentLength, body, sig, source, agcrypto.HeaderSignature and
VerifyHTTPRequest and change the post-read logic to (1) delete the signature
from the appropriate place based on source, (2) restore r.Body to
io.NopCloser(bytes.NewReader(body)), and (3) set r.ContentLength =
origContentLength if origContentLength >= 0 else r.ContentLength =
int64(len(body)).

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.

1 participant