Skip to content

fix: sign the Host header that is actually sent - #81

Open
tedder wants to merge 1 commit into
mainfrom
ted/issue-79-fix
Open

fix: sign the Host header that is actually sent#81
tedder wants to merge 1 commit into
mainfrom
ted/issue-79-fix

Conversation

@tedder

@tedder tedder commented Jul 27, 2026

Copy link
Copy Markdown
Owner

The Host header was signed with the port stripped unconditionally, while the request going out keeps the port whenever it is not the default for the scheme. The two disagreed, so the service computed a different signature and rejected the request with SignatureDoesNotMatch.

This is #79, and before it #34. It was fixed once in 8e1417c (#63) and reverted in b328b86 after #65 reported a regression.

Two changes, which together make the divergence unrepresentable:

  • get_host_header() computes the value the client will send: strip the port only when it is the default for that scheme. 443 is default for https but not for http, so http://host:443 keeps its port. IPv6 literals are no longer split on their first colon, which previously reduced https://[::1] to a single "[" regardless of any port. Credentials are stripped, since they sit in the netloc but are never sent in the Host header -- the old code signed everything before the first colon ('user'), and 8e1417c would have signed them along with the port. Neither matches the wire.

  • call now sets the Host header instead of only signing it. The wire value is byte-identical either way, but signed value and sent value become the same string, so they cannot drift apart again if urllib3 ever changes how it derives the header.

Verified against real SigV4 implementations on non-default ports, which is something AWS itself cannot test: every AWS endpoint is on 443, so this code path is unreachable there. RadosGW on 7480 and MinIO on 9111 both reject the old signature and accept the new one. Real AWS S3 is unaffected, including URLs with an explicit :443.

The host header table gains the credential-bearing URL shapes, and a new test checks them against a real socket rather than trusting the oracle: the oracle models requests stripping credentials before it opens the connection, and that model should itself be tested. Without the fix it reports "AssertionError: 'user' != '127.0.0.1:52809'".

Also flips the assertion added in #68, which was inverted: it asserted the signature did not match the wire, documenting the bug rather than testing the behaviour, while its httpx twin asserted the opposite for the same URL.

Fixes #79
Fixes #34

Claude-Session: https://claude.ai/code/session_01GKqoiAcwVRjYpwpDKcPqDQ

The Host header was signed with the port stripped unconditionally, while
the request going out keeps the port whenever it is not the default for
the scheme. The two disagreed, so the service computed a different
signature and rejected the request with SignatureDoesNotMatch.

This is #79, and before it #34. It was fixed once in 8e1417c (#63) and
reverted in b328b86 after #65 reported a regression.

Two changes, which together make the divergence unrepresentable:

- get_host_header() computes the value the client will send: strip the
  port only when it is the default for that scheme. 443 is default for
  https but not for http, so http://host:443 keeps its port. IPv6
  literals are no longer split on their first colon, which previously
  reduced https://[::1] to a single "[" regardless of any port.
  Credentials are stripped, since they sit in the netloc but are never
  sent in the Host header -- the old code signed everything before the
  first colon ('user'), and 8e1417c would have signed them along with
  the port. Neither matches the wire.

- __call__ now sets the Host header instead of only signing it. The wire
  value is byte-identical either way, but signed value and sent value
  become the same string, so they cannot drift apart again if urllib3
  ever changes how it derives the header.

Verified against real SigV4 implementations on non-default ports, which
is something AWS itself cannot test: every AWS endpoint is on 443, so
this code path is unreachable there. RadosGW on 7480 and MinIO on 9111
both reject the old signature and accept the new one. Real AWS S3 is
unaffected, including URLs with an explicit :443.

The host header table gains the credential-bearing URL shapes, and a new
test checks them against a real socket rather than trusting the oracle:
the oracle models requests stripping credentials before it opens the
connection, and that model should itself be tested. Without the fix it
reports "AssertionError: 'user' != '127.0.0.1:52809'".

Also flips the assertion added in #68, which was inverted: it asserted
the signature did not match the wire, documenting the bug rather than
testing the behaviour, while its httpx twin asserted the opposite for
the same URL.

Fixes #79
Fixes #34

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKqoiAcwVRjYpwpDKcPqDQ
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.

SignatureDoesNotMatch when endpoint URL contains a port Port numbers are required if the port number is not 80/443

1 participant