This is an automated email from the ASF dual-hosted git repository.

kenhuuu pushed a commit to branch empty-params
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 2f63020900e19a23e6fd4eaed1867d8b7cc2234f
Author: Ken Hu <[email protected]>
AuthorDate: Wed Jul 15 18:15:27 2026 -0700

    Fix SigV4 request signing across GLVs to sign a minimal header set CTR
    
    The SigV4 auth interceptors signed transport-managed headers 
(accept-encoding,
    content-type, content-length) that the underlying HTTP stacks add or rewrite
    after signing, producing signatures that no longer matched the bytes sent 
and
    were rejected by SigV4-verifying endpoints. Java, JavaScript and .NET now 
sign
    only host and the headers the AWS SDK adds itself (x-amz-date, the payload 
hash,
    and x-amz-security-token for session credentials); Python already did this. 
Go
    additionally strips the default port (443/80) from the signed and sent Host 
and
    signs its session token.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs      | 32 ++++---
 .../test/Gremlin.Net.UnitTest/Driver/AuthTests.cs  | 50 +++++++++++
 .../tinkerpop/gremlin/driver/auth/Sigv4.java       | 16 ++--
 .../tinkerpop/gremlin/driver/auth/Sigv4Test.java   | 42 +++++++++-
 gremlin-go/driver/auth/auth.go                     | 32 ++++++-
 gremlin-go/driver/auth/auth_test.go                | 97 ++++++++++++++++++++++
 gremlin-js/gremlin-javascript/lib/driver/auth.ts   | 10 ++-
 .../gremlin-javascript/test/unit/auth-test.js      | 46 ++++++++++
 .../main/python/tests/unit/driver/test_sigv4.py    | 82 ++++++++++++++++++
 9 files changed, 377 insertions(+), 30 deletions(-)

diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs
index d713dc9473..69c6e1600a 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Auth.cs
@@ -62,8 +62,8 @@ namespace Gremlin.Net.Driver
         ///     used and the resolved provider is cached on first use (the 
provider itself handles
         ///     credential refresh for expiring credentials like STS).
         /// </summary>
-        /// <param name="region">The AWS region (e.g. "us-east-1").</param>
-        /// <param name="service">The AWS service name (e.g. 
"neptune-db").</param>
+        /// <param name="region">The region.</param>
+        /// <param name="service">The service name.</param>
         /// <param name="credentials">
         ///     Optional AWS credentials. When null, the default credential 
chain is used.
         /// </param>
@@ -125,14 +125,12 @@ namespace Gremlin.Net.Driver
                 OverrideSigningServiceName = 
clientConfig.AuthenticationServiceName,
             };
 
-            // Copy headers (skip Host — signer adds it)
-            foreach (var header in context.Headers)
-            {
-                if (!string.Equals(header.Key, "Host", 
StringComparison.OrdinalIgnoreCase))
-                {
-                    awsRequest.Headers[header.Key] = header.Value;
-                }
-            }
+            // Signed header set: host (derived from the endpoint), 
x-amz-content-sha256 and
+            // x-amz-date, plus x-amz-security-token for session credentials 
(all set below). The
+            // request's own headers (accept, content-type, accept-encoding, 
...) are deliberately
+            // NOT copied into the signature: the HTTP client may add or 
rewrite transport-managed
+            // headers after this interceptor runs, so a signature covering 
them would not match
+            // the bytes sent.
 
             // Copy query parameters
             var query = context.Uri.Query;
@@ -154,6 +152,13 @@ namespace Gremlin.Net.Driver
             var payloadHash = context.GetPayloadHash();
             awsRequest.Headers["x-amz-content-sha256"] = payloadHash;
 
+            // For temporary credentials, add the session token BEFORE signing 
so it is bound into
+            // the signature rather than sent as a weaker present-but-unsigned 
header.
+            if (!string.IsNullOrEmpty(credentials.Token))
+            {
+                awsRequest.Headers["X-Amz-Security-Token"] = credentials.Token;
+            }
+
             // Sign the request
             signer.Sign(awsRequest, clientConfig, new RequestMetrics(), 
credentials);
 
@@ -171,10 +176,11 @@ namespace Gremlin.Net.Driver
             }
             context.Headers["x-amz-content-sha256"] = payloadHash;
 
-            // Add session token if temporary credentials
-            if (!string.IsNullOrEmpty(credentials.Token))
+            // Copy back the session token that was signed above (for 
temporary credentials), so
+            // the header on the wire matches exactly what the signature 
covered.
+            if (awsRequest.Headers.ContainsKey("X-Amz-Security-Token"))
             {
-                context.Headers["X-Amz-Security-Token"] = credentials.Token;
+                context.Headers["X-Amz-Security-Token"] = 
awsRequest.Headers["X-Amz-Security-Token"];
             }
         }
 
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/AuthTests.cs 
b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/AuthTests.cs
index 76c9910d7f..c001c17313 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/AuthTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Driver/AuthTests.cs
@@ -241,5 +241,55 @@ namespace Gremlin.Net.UnitTest.Driver
 
             Assert.Contains("byte[]", ex.Message);
         }
+
+        // Only host and the headers the AWS SDK adds itself are signed. 
Transport-managed headers
+        // (accept-encoding, content-type, ...) are never signed, and the 
session token is signed
+        // only when session credentials are used.
+        private static HttpRequestContext 
CreateSigv4ContextWithTransportHeaders()
+        {
+            return new HttpRequestContext("POST", new 
Uri("https://example.com:8182/gremlin";),
+                new Dictionary<string, string>
+                {
+                    { "Accept", "application/vnd.graphbinary-v4.0" },
+                    { "Content-Type", "application/json" },
+                    { "Accept-Encoding", "deflate" },
+                    { "User-Agent", "gremlin-dotnet-test" },
+                },
+                Encoding.UTF8.GetBytes("{\"gremlin\":\"g.V().count()\"}"));
+        }
+
+        private static string SignedHeaders(HttpRequestContext context)
+        {
+            var authorization = context.Headers["Authorization"];
+            const string marker = "SignedHeaders=";
+            var start = authorization.IndexOf(marker, 
StringComparison.Ordinal) + marker.Length;
+            var end = authorization.IndexOf(',', start);
+            return end < 0 ? authorization.Substring(start) : 
authorization.Substring(start, end - start);
+        }
+
+        [Fact]
+        public async Task 
SigV4AuthShouldSignOnlyMinimalHeaderSetForBasicCredentials()
+        {
+            var interceptor = Auth.Sigv4("region-1", "example-service", 
TestBasicCredentials);
+            var context = CreateSigv4ContextWithTransportHeaders();
+
+            await interceptor(context);
+
+            // The .NET SDK signer includes x-amz-content-sha256 in the signed 
set; that is its
+            // natural behavior and is intentionally left as-is.
+            Assert.Equal("host;x-amz-content-sha256;x-amz-date", 
SignedHeaders(context));
+        }
+
+        [Fact]
+        public async Task 
SigV4AuthShouldSignSecurityTokenForSessionCredentials()
+        {
+            var interceptor = Auth.Sigv4("region-1", "example-service", 
TestSessionCredentials);
+            var context = CreateSigv4ContextWithTransportHeaders();
+
+            await interceptor(context);
+
+            
Assert.Equal("host;x-amz-content-sha256;x-amz-date;x-amz-security-token", 
SignedHeaders(context));
+            Assert.Equal("MOCK_TOKEN", 
context.Headers["X-Amz-Security-Token"]);
+        }
     }
 }
diff --git 
a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4.java
 
b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4.java
index a19e75d749..2dd786a575 100644
--- 
a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4.java
+++ 
b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4.java
@@ -137,18 +137,14 @@ public class Sigv4 implements Auth {
         checkNotNull(request.getUri(), "The request URI must not be null");
         checkNotNull(request.getMethod(), "The request method must not be 
null");
 
-        // convert the headers to the internal API format
-        final Map<String, String> headers = request.headers();
+        // Sign an empty header set: the signer adds (and signs) the Host, 
X-Amz-Date and
+        // X-Amz-Content-Sha256 headers itself (plus X-Amz-Security-Token for 
session
+        // credentials). The request's own headers (accept, content-type, 
accept-encoding,
+        // user-agent, ...) are deliberately NOT signed: transport-managed 
headers may be added or
+        // rewritten after this interceptor runs, so a signature covering them 
would no longer
+        // match the bytes actually sent.
         final Map<String, List<String>> headersInternal = new HashMap<>();
 
-        // we don't want to add the Host header as the Signer always adds the 
host header.
-        for (Map.Entry<String, String> header : headers.entrySet()) {
-            // Skip adding the Host header as the signing process will add one.
-            if (!header.getKey().equalsIgnoreCase(HttpRequest.Headers.HOST)) {
-                headersInternal.put(header.getKey(), 
Collections.singletonList(header.getValue()));
-            }
-        }
-
         // convert the parameters to the internal API format
         final URI uri = request.getUri();
         final Map<String, List<String>> parametersInternal = 
extractParametersFromQueryString(uri.getQuery());
diff --git 
a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4Test.java
 
b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4Test.java
index a72bcd0946..1ccbe3ab4c 100644
--- 
a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4Test.java
+++ 
b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4Test.java
@@ -49,7 +49,7 @@ import static 
software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerCo
 import static 
software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_SECURITY_TOKEN;
 
 public class Sigv4Test {
-    private static final String REGION = "us-west-2";
+    private static final String REGION = "region-1";
     private static final String SERVICE_NAME = "service-name";
     private static final String HOST = "localhost";
     private static final String URI_WITH_QUERY_PARAMS = "http://"; + HOST + 
":8182?a=1&b=2";
@@ -142,4 +142,44 @@ public class Sigv4Test {
                         containsString("/" + REGION + 
"/service-name/aws4_request"),
                         containsString("Signature=")));
     }
+
+    @Test
+    public void shouldSignOnlyMinimalHeaderSetForBasicCredentials() throws 
Exception {
+        
when(credentialsProvider.resolveCredentials()).thenReturn(AwsBasicCredentials.create(KEY,
 SECRET));
+        final HttpRequest httpRequest = createRequestWithTransportHeaders();
+        sigv4.intercept(httpRequest);
+
+        // Only host and the headers the AWS SDK adds itself are signed. 
Transport-managed headers
+        // (accept-encoding, content-type, ...) are never signed, or the 
signature would not match
+        // what the server reconstructs.
+        assertEquals("host;x-amz-content-sha256;x-amz-date", 
signedHeaders(httpRequest));
+    }
+
+    @Test
+    public void shouldSignSecurityTokenForSessionCredentials() throws 
Exception {
+        when(credentialsProvider.resolveCredentials())
+                .thenReturn(AwsSessionCredentials.create(KEY, SECRET, 
"session-token"));
+        final HttpRequest httpRequest = createRequestWithTransportHeaders();
+        sigv4.intercept(httpRequest);
+
+        
assertEquals("host;x-amz-content-sha256;x-amz-date;x-amz-security-token", 
signedHeaders(httpRequest));
+    }
+
+    private HttpRequest createRequestWithTransportHeaders() throws Exception {
+        final byte[] body = "{\"gremlin\":\"g.V().count()\"}".getBytes();
+        final HttpRequest httpRequest = new HttpRequest(new HashMap<>(), body, 
new URI(URI_WITH_QUERY_PARAMS));
+        // Seed transport-managed / content headers that must NOT end up in 
SignedHeaders.
+        httpRequest.headers().put("Accept", 
"application/vnd.graphbinary-v4.0");
+        httpRequest.headers().put("Content-Type", "application/json");
+        httpRequest.headers().put("Accept-Encoding", "deflate");
+        httpRequest.headers().put("User-Agent", "gremlin-java-test");
+        return httpRequest;
+    }
+
+    private static String signedHeaders(final HttpRequest httpRequest) {
+        final String authorization = httpRequest.headers().get(AUTHORIZATION);
+        final int start = authorization.indexOf("SignedHeaders=") + 
"SignedHeaders=".length();
+        final int end = authorization.indexOf(',', start);
+        return authorization.substring(start, end < 0 ? authorization.length() 
: end);
+    }
 }
diff --git a/gremlin-go/driver/auth/auth.go b/gremlin-go/driver/auth/auth.go
index d24330179d..15e16e0094 100644
--- a/gremlin-go/driver/auth/auth.go
+++ b/gremlin-go/driver/auth/auth.go
@@ -26,6 +26,8 @@ import (
        "context"
        "encoding/base64"
        "fmt"
+       "net/http"
+       "strings"
        "sync"
        "time"
 
@@ -98,17 +100,39 @@ func SigV4WithCredentials(region, service string, 
credentialsProvider aws.Creden
                        return err
                }
 
-               stdReq, err := req.ToStdRequest()
+               // Signed header set: host, x-amz-date, x-amz-content-sha256, 
and (for session
+               // credentials) x-amz-security-token. The request's own headers 
(accept, content-type,
+               // ...) are deliberately NOT signed: the HTTP client manages 
transport headers such as
+               // Content-Length and Accept-Encoding after this interceptor 
runs, so a signature covering
+               // them would not match the bytes actually sent.
+
+               // Strip the default port (443 for https, 80 for http) from the 
host so the signed
+               // canonical Host and the Host header sent on the wire both 
omit it, matching what a
+               // spec-compliant verifier reconstructs (a bare host). Mutating 
req.URL here is safe: the
+               // driver parses a fresh URL per request, and this also fixes 
the Host sent on the wire.
+               // Trim only the trailing ":port" so IPv6 literals keep their 
brackets (using
+               // URL.Hostname() would drop them, yielding an unparseable host 
like "::1").
+               port := req.URL.Port()
+               if (req.URL.Scheme == "https" && port == "443") || 
(req.URL.Scheme == "http" && port == "80") {
+                       req.URL.Host = strings.TrimSuffix(req.URL.Host, 
":"+port)
+               }
+
+               payloadHash := req.PayloadHash()
+               stdReq, err := http.NewRequest(req.Method, req.URL.String(), 
nil)
                if err != nil {
                        return err
                }
-               stdReq.Body = nil // Body is handled separately via payload hash
+               stdReq.Host = req.URL.Host
+               // Set the payload hash header BEFORE signing so it is part of 
the signed set:
+               // aws-sdk-go-v2's SignHTTP takes the hash as a parameter but 
does not add the header
+               // itself, and a present-but-unsigned header is rejected by the 
server.
+               stdReq.Header.Set("X-Amz-Content-Sha256", payloadHash)
 
-               if err := signer.SignHTTP(ctx, creds, stdReq, 
req.PayloadHash(), service, region, time.Now()); err != nil {
+               if err := signer.SignHTTP(ctx, creds, stdReq, payloadHash, 
service, region, time.Now()); err != nil {
                        return err
                }
 
-               // Copy signed headers back to HttpRequest
+               // Copy the SigV4 output headers back onto the request.
                for k, v := range stdReq.Header {
                        req.Headers[k] = v
                }
diff --git a/gremlin-go/driver/auth/auth_test.go 
b/gremlin-go/driver/auth/auth_test.go
index 26779d7262..6a44aa3993 100644
--- a/gremlin-go/driver/auth/auth_test.go
+++ b/gremlin-go/driver/auth/auth_test.go
@@ -22,6 +22,7 @@ package auth
 import (
        "context"
        "encoding/base64"
+       "net/url"
        "strings"
        "testing"
 
@@ -140,3 +141,99 @@ func TestSigV4(t *testing.T) {
                assert.Contains(t, req.Headers.Get("Authorization"), 
"AWS4-HMAC-SHA256")
        })
 }
+
+// signedHeadersFromAuth extracts the SignedHeaders list from an Authorization 
header value.
+func signedHeadersFromAuth(authHeader string) string {
+       const marker = "SignedHeaders="
+       idx := strings.Index(authHeader, marker)
+       if idx < 0 {
+               return ""
+       }
+       rest := authHeader[idx+len(marker):]
+       if end := strings.Index(rest, ","); end >= 0 {
+               return rest[:end]
+       }
+       return rest
+}
+
+// TestSigV4SignedHeaders pins the signed header set: only host and the 
headers the AWS SDK adds
+// itself are signed, transport-managed headers such as accept-encoding are 
never signed even when
+// present on the request, and the session token is signed only when session 
credentials are used.
+func TestSigV4SignedHeaders(t *testing.T) {
+       // A default-port (443) https URL so the test also covers host:port 
stripping: the signed Host
+       // must be the bare hostname, matching what a spec-compliant verifier 
reconstructs.
+       newRequest := func() *gremlingo.HttpRequest {
+               req, err := gremlingo.NewHttpRequest("POST", 
"https://example.com:443/gremlin";)
+               assert.NoError(t, err)
+               // Seed transport-managed and content headers that must NOT end 
up signed.
+               req.Headers.Set("Accept", graphBinaryMimeType)
+               req.Headers.Set("Content-Type", "application/json")
+               req.Headers.Set("Accept-Encoding", "deflate")
+               req.Headers.Set("User-Agent", "gremlin-go-test")
+               req.Body = []byte(`{"gremlin":"g.V().count()"}`)
+               return req
+       }
+
+       t.Run("basic credentials sign only host, date, content-sha256", func(t 
*testing.T) {
+               req := newRequest()
+               provider := &mockCredentialsProvider{accessKey: "MOCK_ID", 
secretKey: "MOCK_KEY"}
+               err := SigV4WithCredentials("region-1", "example-service", 
provider)(req)
+               assert.NoError(t, err)
+
+               signedHeaders := 
signedHeadersFromAuth(req.Headers.Get("Authorization"))
+               // aws-sdk-go-v2 leaves x-amz-content-sha256 in the signed set 
(see auth.go); that is the
+               // SDK's natural behavior and is intentionally left as-is.
+               assert.Equal(t, "host;x-amz-content-sha256;x-amz-date", 
signedHeaders)
+               assert.NotContains(t, signedHeaders, "accept-encoding")
+               assert.NotContains(t, signedHeaders, "content-type")
+               assert.NotContains(t, signedHeaders, "x-amz-security-token")
+
+               // The signed (and sent) Host must omit the default :443 port.
+               assert.Equal(t, "example.com", req.URL.Host)
+       })
+
+       t.Run("session credentials also sign the security token", func(t 
*testing.T) {
+               req := newRequest()
+               provider := &mockCredentialsProvider{accessKey: "MOCK_ID", 
secretKey: "MOCK_KEY", sessionToken: "MOCK_TOKEN"}
+               err := SigV4WithCredentials("region-1", "example-service", 
provider)(req)
+               assert.NoError(t, err)
+
+               signedHeaders := 
signedHeadersFromAuth(req.Headers.Get("Authorization"))
+               assert.Equal(t, 
"host;x-amz-content-sha256;x-amz-date;x-amz-security-token", signedHeaders)
+               assert.Equal(t, "MOCK_TOKEN", 
req.Headers.Get("X-Amz-Security-Token"))
+       })
+}
+
+// TestSigV4HostPortStripping verifies that the default port is removed from 
the host for signing
+// while non-default ports are preserved, and that IPv6 literals keep their 
brackets (stripping the
+// port must not corrupt the host into an unparseable form).
+func TestSigV4HostPortStripping(t *testing.T) {
+       cases := []struct {
+               url      string
+               wantHost string
+       }{
+               {"https://example.com:443/gremlin";, "example.com"},
+               {"http://example.com:80/gremlin";, "example.com"},
+               {"https://example.com:8182/gremlin";, "example.com:8182"},
+               {"https://example.com/gremlin";, "example.com"},
+               {"https://[::1]:443/gremlin";, "[::1]"},
+               {"https://[2001:db8::1]:443/gremlin";, "[2001:db8::1]"},
+               {"https://[::1]:8182/gremlin";, "[::1]:8182"},
+       }
+       for _, tc := range cases {
+               t.Run(tc.url, func(t *testing.T) {
+                       req, err := gremlingo.NewHttpRequest("POST", tc.url)
+                       assert.NoError(t, err)
+                       req.Body = []byte(`{"gremlin":"g.V()"}`)
+                       provider := &mockCredentialsProvider{accessKey: 
"MOCK_ID", secretKey: "MOCK_KEY"}
+
+                       err = SigV4WithCredentials("region-1", 
"example-service", provider)(req)
+                       assert.NoError(t, err)
+                       assert.Equal(t, tc.wantHost, req.URL.Host)
+                       // The mutated URL must remain parseable (an IPv6 host 
that lost its brackets would
+                       // fail to re-parse, breaking the wire request built 
from req.URL.String()).
+                       _, parseErr := url.Parse(req.URL.String())
+                       assert.NoError(t, parseErr)
+               })
+       }
+}
diff --git a/gremlin-js/gremlin-javascript/lib/driver/auth.ts 
b/gremlin-js/gremlin-javascript/lib/driver/auth.ts
index 4d14f7c4f3..f5b32739dc 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/auth.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/auth.ts
@@ -65,6 +65,11 @@ export function sigv4(region: string, service: string, 
credentialsProvider?: Aws
     }
 
     const url = new URL(request.url);
+    // Sign only the Host header; the signer itself adds (and signs) 
x-amz-date, the payload hash,
+    // and x-amz-security-token for session credentials. The request's own 
headers (accept,
+    // content-type, ...) are deliberately NOT fed into the signature: the 
HTTP stack may add or
+    // rewrite transport-managed headers such as accept-encoding after this 
interceptor runs, so a
+    // signature covering them would no longer match the bytes actually sent.
     const signed = await signer.sign({
       method: request.method,
       protocol: url.protocol,
@@ -72,12 +77,13 @@ export function sigv4(region: string, service: string, 
credentialsProvider?: Aws
       port: url.port ? Number(url.port) : undefined,
       path: url.pathname + url.search,
       headers: {
-        ...request.headers,
         host: url.host,
       },
       body: request.body,
     });
 
-    request.headers = signed.headers;
+    // Merge the signed auth headers onto the request; the request's other 
headers (accept,
+    // content-type, ...) still reach the wire, they are simply not part of 
the signature.
+    request.headers = { ...request.headers, ...signed.headers };
   };
 }
diff --git a/gremlin-js/gremlin-javascript/test/unit/auth-test.js 
b/gremlin-js/gremlin-javascript/test/unit/auth-test.js
index e35fb1f8f4..2ad4e30510 100644
--- a/gremlin-js/gremlin-javascript/test/unit/auth-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/auth-test.js
@@ -115,5 +115,51 @@ describe('auth', function () {
       // Signing adds at least authorization and x-amz-date on top of the 
originals
       assert.ok(Object.keys(request.headers).length >= preSignKeys.length + 2);
     });
+
+    // Extracts the SignedHeaders list from an Authorization header value.
+    function signedHeaders(authHeader) {
+      const marker = 'SignedHeaders=';
+      const start = authHeader.indexOf(marker) + marker.length;
+      const end = authHeader.indexOf(',', start);
+      return end < 0 ? authHeader.substring(start) : 
authHeader.substring(start, end);
+    }
+
+    // Only host and the headers the AWS SDK adds itself are signed. 
Transport-managed headers
+    // (accept-encoding, content-type, ...) are never signed, and the session 
token is signed only
+    // when session credentials are used.
+    function createRequestWithTransportHeaders() {
+      return new HttpRequest('POST', 'https://example.com:8182/gremlin', {
+        'accept': 'application/vnd.graphbinary-v4.0',
+        'content-type': 'application/json',
+        'accept-encoding': 'deflate',
+        'user-agent': 'gremlin-js-test',
+      }, Buffer.from('{"gremlin":"g.V().count()"}'));
+    }
+
+    it('should sign only the minimal header set for basic credentials', async 
function () {
+      const request = createRequestWithTransportHeaders();
+      const interceptor = sigv4('region-1', 'example-service', mockProvider);
+      await interceptor(request);
+
+      // smithy's SignatureV4 includes x-amz-content-sha256 in the signed set; 
that is its
+      // natural behavior and is intentionally left as-is.
+      assert.strictEqual(signedHeaders(request.headers['authorization']),
+        'host;x-amz-content-sha256;x-amz-date');
+    });
+
+    it('should sign the security token for session credentials', async 
function () {
+      const providerWithToken = () => ({
+        accessKeyId: 'MOCK_ACCESS_KEY',
+        secretAccessKey: 'MOCK_SECRET_KEY',
+        sessionToken: 'MOCK_SESSION_TOKEN',
+      });
+      const request = createRequestWithTransportHeaders();
+      const interceptor = sigv4('region-1', 'example-service', 
providerWithToken);
+      await interceptor(request);
+
+      assert.strictEqual(signedHeaders(request.headers['authorization']),
+        'host;x-amz-content-sha256;x-amz-date;x-amz-security-token');
+      assert.strictEqual(request.headers['x-amz-security-token'], 
'MOCK_SESSION_TOKEN');
+    });
   });
 });
diff --git a/gremlin-python/src/main/python/tests/unit/driver/test_sigv4.py 
b/gremlin-python/src/main/python/tests/unit/driver/test_sigv4.py
new file mode 100644
index 0000000000..39eddf7426
--- /dev/null
+++ b/gremlin-python/src/main/python/tests/unit/driver/test_sigv4.py
@@ -0,0 +1,82 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+"""SigV4 SignedHeaders invariants.
+
+Only host and the headers the AWS SDK adds itself are signed; 
transport-managed headers
+(accept-encoding, content-type, ...) are never signed, or the signature would 
not match what
+the server reconstructs. The session token is signed only when session 
credentials are used.
+
+Unlike the other GLVs, botocore's plain SigV4Auth does not add an 
``x-amz-content-sha256``
+header, so Python's SignedHeaders is ``host;x-amz-date`` (the body hash is 
still bound into the
+signature via the canonical request's mandatory payload-hash line). This is 
the SDK's natural
+behavior and is intentionally left as-is.
+"""
+from botocore.credentials import Credentials
+
+from gremlin_python.driver.auth import sigv4
+from gremlin_python.driver.http_request import HttpRequest
+
+ACCESS_KEY = "foo"
+SECRET_KEY = "bar"
+
+
+def _signed_headers(request):
+    lower = {k.lower(): v for k, v in request.headers.items()}
+    authorization = lower["authorization"]
+    marker = "SignedHeaders="
+    start = authorization.index(marker) + len(marker)
+    end = authorization.find(",", start)
+    return authorization[start:] if end < 0 else authorization[start:end]
+
+
+def _make_request():
+    # A default-port (443) https URL; seed transport-managed / content headers 
that must NOT be
+    # signed.
+    return HttpRequest(
+        method="POST",
+        url="https://example.com:443/gremlin";,
+        headers={
+            "accept": "application/vnd.graphbinary-v4.0",
+            "content-type": "application/json",
+            "accept-encoding": "deflate",
+            "user-agent": "gremlin-python-test",
+        },
+        body=b'{"gremlin":"g.V().count()"}',
+    )
+
+
+class TestSigV4SignedHeaders:
+
+    def test_basic_credentials_sign_only_host_and_date(self):
+        creds = Credentials(access_key=ACCESS_KEY, secret_key=SECRET_KEY, 
token=None)
+        request = _make_request()
+
+        sigv4("region-1", "example-service", credentials=creds)(request)
+
+        assert _signed_headers(request) == "host;x-amz-date"
+
+    def test_session_credentials_also_sign_the_security_token(self):
+        creds = Credentials(access_key=ACCESS_KEY, secret_key=SECRET_KEY, 
token="MOCK_TOKEN")
+        request = _make_request()
+
+        sigv4("region-1", "example-service", credentials=creds)(request)
+
+        assert _signed_headers(request) == 
"host;x-amz-date;x-amz-security-token"
+        lower = {k.lower(): v for k, v in request.headers.items()}
+        assert lower["x-amz-security-token"] == "MOCK_TOKEN"

Reply via email to