Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions src/openai/lib/_bedrock_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ def get_credentials(self) -> object | None: ...
"x-amz-date",
"x-amz-security-token",
)
# 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]:
Expand Down Expand Up @@ -142,7 +164,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 _UNSIGNED_HEADERS
}
signed_headers["X-Amz-Content-SHA256"] = hashlib.sha256(body or b"").hexdigest()
aws_request = self._aws_request_cls(
Expand All @@ -160,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(
Expand Down
53 changes: 53 additions & 0 deletions tests/lib/test_bedrock_auth_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,59 @@ def test_shared_sigv4_fixture_matches_node(monkeypatch: pytest.MonkeyPatch) -> N
assert signed_headers["x-amz-date"] == fixture["expected"]["date"]


# 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",
source="static",
access_key_id="AKIDEXAMPLE",
secret_access_key="wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
session_token="session-token",
)
)
# 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(";")

# 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"])
def test_auth_selection_fixture(case: dict[str, Any], monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
Expand Down