From d8b15cd8aeb75760076ec707919039809d76bd00 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Sun, 2 Aug 2026 20:58:52 +0500 Subject: [PATCH 1/2] fix(bedrock): exclude connection header from SigV4 signing --- src/openai/lib/_bedrock_auth.py | 5 +++- tests/lib/test_bedrock_auth_conformance.py | 28 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/openai/lib/_bedrock_auth.py b/src/openai/lib/_bedrock_auth.py index 2921858195..e067197af3 100644 --- a/src/openai/lib/_bedrock_auth.py +++ b/src/openai/lib/_bedrock_auth.py @@ -23,6 +23,7 @@ def get_credentials(self) -> object | None: ... "x-amz-date", "x-amz-security-token", ) +_HOP_BY_HOP_HEADERS = ("connection",) def _load_botocore() -> tuple[Any, Any, Any, Any]: @@ -142,7 +143,9 @@ def sign(self, *, method: str, url: str, headers: Mapping[str, str], body: bytes credentials = get_frozen_credentials() signed_headers = { - name: value for name, value in headers.items() if name.lower() not in _AWS_SIGNING_HEADERS + name: value + for name, value in headers.items() + if name.lower() not in _AWS_SIGNING_HEADERS and name.lower() not in _HOP_BY_HOP_HEADERS } signed_headers["X-Amz-Content-SHA256"] = hashlib.sha256(body or b"").hexdigest() aws_request = self._aws_request_cls( diff --git a/tests/lib/test_bedrock_auth_conformance.py b/tests/lib/test_bedrock_auth_conformance.py index 7478b08cbd..842e87ee8a 100644 --- a/tests/lib/test_bedrock_auth_conformance.py +++ b/tests/lib/test_bedrock_auth_conformance.py @@ -113,6 +113,34 @@ def test_shared_sigv4_fixture_matches_node(monkeypatch: pytest.MonkeyPatch) -> N assert signed_headers["x-amz-date"] == fixture["expected"]["date"] +def test_sign_excludes_connection_header() -> None: + auth = BedrockAwsAuth( + BedrockAwsAuthConfig( + region="us-east-1", + source="static", + access_key_id="AKIDEXAMPLE", + secret_access_key="wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + session_token="session-token", + ) + ) + signed_headers = _lower_headers( + auth.sign( + method="POST", + url="https://bedrock-mantle.us-east-1.api.aws/openai/v1/responses", + headers={ + "content-type": "application/json", + "host": "bedrock-mantle.us-east-1.api.aws", + "connection": "keep-alive", + }, + body=b"{}", + ) + ) + signed_header_names = signed_headers["authorization"].split("SignedHeaders=", 1)[1].split(",", 1)[0].split(";") + + assert "connection" not in signed_headers + assert "connection" not in signed_header_names + + @pytest.mark.parametrize("case", _cases("auth_selection"), ids=lambda case: case["id"]) def test_auth_selection_fixture(case: dict[str, Any], monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False) From 70558516069917a22157c5c49ebe068cb73ff599 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Tue, 4 Aug 2026 05:41:47 +0500 Subject: [PATCH 2/2] fix(bedrock): exclude full volatile transport-header set from SigV4 signing --- src/openai/lib/_bedrock_auth.py | 35 ++++++++++++-- tests/lib/test_bedrock_auth_conformance.py | 53 ++++++++++++++++------ 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/openai/lib/_bedrock_auth.py b/src/openai/lib/_bedrock_auth.py index e067197af3..9edb9c03c4 100644 --- a/src/openai/lib/_bedrock_auth.py +++ b/src/openai/lib/_bedrock_auth.py @@ -23,7 +23,28 @@ def get_credentials(self) -> object | None: ... "x-amz-date", "x-amz-security-token", ) -_HOP_BY_HOP_HEADERS = ("connection",) +# Volatile transport headers that intermediaries (proxies, load balancers, and +# other nodes) may add or rewrite in transit. AWS SigV4 guidance is to exclude +# these from the signature so such rewrites do not invalidate it. This mirrors +# botocore's own SIGNED_HEADERS_BLACKLIST so signing behavior is identical across +# the supported botocore>=1.40.0 range, including releases that predate the +# equivalent upstream fix (boto/botocore#3643). +# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html +_UNSIGNED_HEADERS = frozenset( + { + "connection", + "expect", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "user-agent", + "x-amzn-trace-id", + } +) def _load_botocore() -> tuple[Any, Any, Any, Any]: @@ -145,7 +166,7 @@ def sign(self, *, method: str, url: str, headers: Mapping[str, str], body: bytes signed_headers = { name: value for name, value in headers.items() - if name.lower() not in _AWS_SIGNING_HEADERS and name.lower() not in _HOP_BY_HOP_HEADERS + if name.lower() not in _AWS_SIGNING_HEADERS and name.lower() not in _UNSIGNED_HEADERS } signed_headers["X-Amz-Content-SHA256"] = hashlib.sha256(body or b"").hexdigest() aws_request = self._aws_request_cls( @@ -163,7 +184,15 @@ def sign(self, *, method: str, url: str, headers: Mapping[str, str], body: bytes "or runtime identity configuration and try again." ) from exc - return dict(aws_request.headers.items()) + result = dict(aws_request.headers.items()) + # Preserve the caller's volatile transport headers (e.g. an explicit + # `Connection: close`) on the outgoing request. They were excluded from + # the signature above, not from the request itself, matching how botocore + # leaves blacklisted headers on the request while keeping them unsigned. + for name, value in headers.items(): + if name.lower() in _UNSIGNED_HEADERS and name not in result: + result[name] = value + return result def resolve_aws_region_with_source( diff --git a/tests/lib/test_bedrock_auth_conformance.py b/tests/lib/test_bedrock_auth_conformance.py index 842e87ee8a..b3c12857de 100644 --- a/tests/lib/test_bedrock_auth_conformance.py +++ b/tests/lib/test_bedrock_auth_conformance.py @@ -113,7 +113,28 @@ def test_shared_sigv4_fixture_matches_node(monkeypatch: pytest.MonkeyPatch) -> N assert signed_headers["x-amz-date"] == fixture["expected"]["date"] -def test_sign_excludes_connection_header() -> None: +# Volatile transport headers that intermediaries may add or rewrite in transit. +# These must be excluded from the SigV4 signature (but left on the request) so a +# rewrite does not cause a signature mismatch. Mirrors botocore's +# SIGNED_HEADERS_BLACKLIST and the AWS SigV4 guidance: +# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html +_UNSIGNED_TRANSPORT_HEADERS = [ + "connection", + "expect", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "user-agent", + "x-amzn-trace-id", +] + + +@pytest.mark.parametrize("header_name", _UNSIGNED_TRANSPORT_HEADERS) +def test_sign_excludes_volatile_transport_header(header_name: str) -> None: auth = BedrockAwsAuth( BedrockAwsAuthConfig( region="us-east-1", @@ -123,22 +144,26 @@ def test_sign_excludes_connection_header() -> None: session_token="session-token", ) ) - signed_headers = _lower_headers( - auth.sign( - method="POST", - url="https://bedrock-mantle.us-east-1.api.aws/openai/v1/responses", - headers={ - "content-type": "application/json", - "host": "bedrock-mantle.us-east-1.api.aws", - "connection": "keep-alive", - }, - body=b"{}", - ) + # Send the header with a mixed-case name to exercise case-insensitive matching. + mixed_case_name = header_name.title() + result = auth.sign( + method="POST", + url="https://bedrock-mantle.us-east-1.api.aws/openai/v1/responses", + headers={ + "content-type": "application/json", + "host": "bedrock-mantle.us-east-1.api.aws", + mixed_case_name: "example-value", + }, + body=b"{}", ) + signed_headers = _lower_headers(result) signed_header_names = signed_headers["authorization"].split("SignedHeaders=", 1)[1].split(",", 1)[0].split(";") - assert "connection" not in signed_headers - assert "connection" not in signed_header_names + # Excluded from the signature so an intermediary rewriting it cannot break signing. + assert header_name not in signed_header_names + # But preserved on the outgoing request (unsigned), matching botocore's behavior. + assert header_name in signed_headers + assert signed_headers[header_name] == "example-value" @pytest.mark.parametrize("case", _cases("auth_selection"), ids=lambda case: case["id"])