From 750d6d6670eafcf7b22efbbe7521b0241facc6c7 Mon Sep 17 00:00:00 2001 From: suhridsaran Date: Sat, 6 Dec 2025 10:20:56 -0800 Subject: [PATCH] feat(tools): add aws sigv4 signing tool --- aws_sigv4.md | 57 ++++++++++++++++ src/strands/tools/aws_sigv4.py | 93 +++++++++++++++++++++++++++ tests/strands/tools/test_aws_sigv4.py | 25 +++++++ 3 files changed, 175 insertions(+) create mode 100644 aws_sigv4.md create mode 100644 src/strands/tools/aws_sigv4.py create mode 100644 tests/strands/tools/test_aws_sigv4.py diff --git a/aws_sigv4.md b/aws_sigv4.md new file mode 100644 index 000000000..5f97faf26 --- /dev/null +++ b/aws_sigv4.md @@ -0,0 +1,57 @@ +# AWS SigV4 Signing Tool + +## Description +`AwsSigV4Tool` produces AWS Signature Version 4 headers for HTTP requests. +It can be used with the Strands HTTP tool to securely call any AWS service without external dependencies. + +## Usage + +``` +from aws_sigv4_tool import AwsSigV4Tool + +tool = AwsSigV4Tool() + +signed_headers = tool( + method="GET", + service="s3", + region="us-east-1", + host="examplebucket.s3.amazonaws.com", + uri="/test.txt", + query="", + headers={}, + body="", + access_key="AKIA...", + secret_key="SECRET..." +) +# Use signed_headers["headers"] with the Strands HTTP tool +``` +## Parameters + +| Name | Type | Description | +| ---------- | ---- | ----------------------------- | +| method | str | HTTP method (GET, POST, etc.) | +| service | str | AWS service name (e.g., s3) | +| region | str | AWS region (e.g., us-east-1) | +| host | str | AWS service endpoint host | +| uri | str | Request URI (path) | +| query | str | Query string | +| headers | dict | Existing headers to include | +| body | str | Request body | +| access_key | str | AWS Access Key ID | +| secret_key | str | AWS Secret Access Key | + +## Returns + +``` +{ + "headers": { + "x-amz-date": "20251130T190000Z", + "Authorization": "AWS4-HMAC-SHA256 Credential=..." + } +} +``` + +## Notes + Fully compatible with Strands HTTP tool. + No external dependencies; pure Python. + Safe for testing — no actual AWS credentials required. \ No newline at end of file diff --git a/src/strands/tools/aws_sigv4.py b/src/strands/tools/aws_sigv4.py new file mode 100644 index 000000000..8d5190536 --- /dev/null +++ b/src/strands/tools/aws_sigv4.py @@ -0,0 +1,93 @@ +from datetime import datetime +import hashlib +import hmac +from typing import Dict + + +class AwsSigV4Tool: + """ + Tool: aws_sigv4 + Description: Produces AWS Signature Version 4 headers for an HTTP request. + + Usage: + tool = AwsSigV4Tool() + headers = tool( + method="GET", + service="s3", + region="us-east-1", + host="examplebucket.s3.amazonaws.com", + uri="/test.txt", + query="", + headers={}, + body="", + access_key="AKIA...", + secret_key="SECRET..." + ) + """ + + def __call__( + self, + method: str, + service: str, + region: str, + host: str, + uri: str, + query: str, + headers: Dict[str, str], + body: str, + access_key: str, + secret_key: str, + ) -> Dict[str, Dict[str, str]]: + + def _sign(key, msg): + return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() + + def _get_signature_key(key, date_stamp, region_name, service_name): + k_date = _sign(("AWS4" + key).encode("utf-8"), date_stamp) + k_region = _sign(k_date, region_name) + k_service = _sign(k_region, service_name) + k_signing = _sign(k_service, "aws4_request") + return k_signing + + # Step 1: Prepare timestamps + t = datetime.utcnow() + amz_date = t.strftime("%Y%m%dT%H%M%SZ") + date_stamp = t.strftime("%Y%m%d") + + # Step 2: Canonical request + canonical_uri = uri + canonical_querystring = query + canonical_headers = f"host:{host}\n" + f"x-amz-date:{amz_date}\n" + signed_headers = "host;x-amz-date" + payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest() + canonical_request = ( + f"{method}\n{canonical_uri}\n{canonical_querystring}\n" + f"{canonical_headers}\n{signed_headers}\n{payload_hash}" + ) + + # Step 3: String to sign + algorithm = "AWS4-HMAC-SHA256" + credential_scope = f"{date_stamp}/{region}/{service}/aws4_request" + string_to_sign = ( + f"{algorithm}\n{amz_date}\n{credential_scope}\n" + f"{hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()}" + ) + + # Step 4: Signing key + signing_key = _get_signature_key(secret_key, date_stamp, region, service) + + # Step 5: Signature + signature = hmac.new( + signing_key, string_to_sign.encode("utf-8"), hashlib.sha256 + ).hexdigest() + + # Step 6: Authorization header + authorization_header = ( + f"{algorithm} Credential={access_key}/{credential_scope}, " + f"SignedHeaders={signed_headers}, Signature={signature}" + ) + + headers["x-amz-date"] = amz_date + headers["Authorization"] = authorization_header + + return {"headers": headers} diff --git a/tests/strands/tools/test_aws_sigv4.py b/tests/strands/tools/test_aws_sigv4.py new file mode 100644 index 000000000..5d76deb43 --- /dev/null +++ b/tests/strands/tools/test_aws_sigv4.py @@ -0,0 +1,25 @@ +import pytest +from strands.tools.aws_sigv4 import AwsSigV4Tool + + +def test_sigv4_returns_headers(): + tool = AwsSigV4Tool() + result = tool( + method="GET", + service="service", + region="us-east-1", + host="example.com", + uri="/", + query="", + headers={}, + body="", + access_key="AKIAEXAMPLE", + secret_key="SECRETEXAMPLE", + ) + + assert "headers" in result + headers = result["headers"] + assert "Authorization" in headers + assert "x-amz-date" in headers + # basic format checks + assert headers["Authorization"].startswith("AWS4-HMAC-SHA256")