AWS Developer Tools Blog

Introducing a standalone SigV4 signer for the AWS SDK for .NET

Until now, signing an AWS request that no generated SDK client covered meant reaching into internal SDK types or hand-writing the AWS Signature Version 4 (SigV4) algorithm.

The AWS SDK for .NET now includes a public SigV4 signer you can call directly. Use the signer to add SigV4 authentication to an HTTP request, or to produce a presigned URL, for an AWS use case that has no generated service client. This post shows how to sign a request, create a presigned URL, and sign every request on an HttpClient automatically.

When to use the SigV4 signer

The standalone signer does not replace the generated service clients. If a client exists for your call, keep using that client: it signs for you and handles retries, endpoints, and serialization. Some AWS use cases have no generated client, and that is what the signer is for:

Getting started

The signer has no setup beyond a single package reference. This section walks through adding the dependency, signing a request, and creating a presigned URL.

Add the dependency

The signer shipped in AWSSDK.Core version 4.0.101.0. Because AWSSDK.Core is a dependency of every service package, most projects already reference it, so make sure you are on 4.0.101.0 or later. To add or update it directly, use the following .NET CLI command:

dotnet add package AWSSDK.Core --version 4.0.101.0

Sign a request

SigV4 signing takes two inputs: an AWSSigningRequest that describes the HTTP request, and an AWSSigV4Parameters that says who is signing, for which service, and in which region. The signer returns the headers to add to your outbound request. The following example signs a GetCallerIdentity call to AWS Security Token Service and sends it with HttpClient:

using Amazon;
using Amazon.Runtime.Credentials;
using Amazon.Runtime.Signing;

var signingRequest = new AWSSigningRequest
{
    HttpMethod = HttpMethod.Get,
    RequestUri = new Uri("https://sts.us-east-1.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15"),
};

var parameters = new AWSSigV4Parameters
{
    // Leave Credentials or Region null to resolve them from the default
    // credential and region resolution chains, the same way a service client does.
    Credentials = DefaultAWSCredentialsIdentityResolver.GetCredentials(),
    Region = RegionEndpoint.USEast1,
    Service = "sts",
};

AWSSigningResult result = await AWSSigV4Signer.SignAsync(signingRequest, parameters);

var message = new HttpRequestMessage(HttpMethod.Get, signingRequest.RequestUri);
foreach (var header in result.Headers)
    message.Headers.TryAddWithoutValidation(header.Key, header.Value);

using var http = new HttpClient();
using var response = await http.SendAsync(message);

Apply every header in result.Headers, not just Authorization. The result includes X-Amz-Date and, for temporary credentials, X-Amz-Security-Token. Both are covered by the signature, so a request carrying only Authorization is rejected.

To find the service name for an AWS service, read the AuthenticationServiceName property on that service’s config object. For example, use the following to look up the name for Amazon Simple Storage Service (Amazon S3):

var serviceName = new AmazonS3Config().AuthenticationServiceName; // "s3"

The two most common standalone-signer use cases have no config object to read, so use these names directly: for Amazon API Gateway (invoking a deployed REST or HTTP API) the service name is execute-api, and for an AWS Lambda function URL it is lambda.

Create a presigned URL

PresignAsync moves all the authentication into the query string and returns a URL that carries its own signature. Anyone who holds the URL can send the request until it expires, up to a maximum of 7 days. The following example presigns the same request and fetches it with no extra headers:

PresignResult presigned = await AWSSigV4Signer.PresignAsync(
    signingRequest, parameters, TimeSpan.FromMinutes(15));

// The URL carries all auth in the query string, so send it with no extra headers.
using var http = new HttpClient();
using var response = await http.GetAsync(presigned.Uri);

If you signed extra headers beyond host, PresignResult.SignedHeaders lists them. Whoever sends the URL must resend those headers, or the service rejects the request. When you presign with temporary credentials, the URL cannot outlive the credentials session no matter what expiry you request.

Sign every request with SigV4SigningHandler

SigV4SigningHandler signs every request an HttpClient sends, so you set up signing once and then write ordinary HttpClient code. It is a DelegatingHandler that you install on the client with a default service and region, which you can override per request (as described in Override the service or region per request). The following example installs the handler and posts a JSON body that is signed automatically, including the body hash:

using Amazon;
using Amazon.Runtime.Credentials;
using Amazon.Runtime.Signing;

var handler = new SigV4SigningHandler(
    DefaultAWSCredentialsIdentityResolver.GetCredentials(),
    RegionEndpoint.USEast1,
    service: "execute-api");

using var http = new HttpClient(handler);

var response = await http.PostAsync(
    "https://abc123.execute-api.us-east-1.amazonaws.com/prod/items",
    new StringContent("""{ "name": "widget" }""", Encoding.UTF8, "application/json"));

The example signs for execute-api, the service name used to invoke a deployed API Gateway API.

The handler signs each request just before it is sent, and a SigV4 signature covers the exact URL, headers, and timestamp. Anything that changes or replays a request after signing breaks the signature, so watch for two cases:

  • Redirects: the handler’s default transport turns automatic redirects off. An automatically followed redirect would be sent to the new location unsigned and rejected. If you need to follow redirects, do it so each request passes through the handler and is signed for its actual destination.
  • Retries: sign every attempt fresh rather than resending the first signature. A replayed signature can fall outside the allowed clock-skew window and be rejected.

Register it with IHttpClientFactory

In an application that uses dependency injection, register the handler through IHttpClientFactory. Under IHttpClientFactory, the factory creates the innermost handler that actually sends the request, so the signing handler cannot turn off redirects for you the way it does otherwise. Turn them off yourself on that handler with ConfigurePrimaryHttpMessageHandler, as shown:

services.AddTransient(_ => new SigV4SigningHandler(
    DefaultAWSCredentialsIdentityResolver.GetCredentials(),
    RegionEndpoint.USEast1,
    service: "execute-api"));

services.AddHttpClient("signed")
    .AddHttpMessageHandler<SigV4SigningHandler>()
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false });

Override the service or region per request

One handler can serve more than one service or region. To override the handler defaults for a single request, set an option on that HttpRequestMessage:

var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Options.Set(new HttpRequestOptionsKey<string>(SigV4SigningHandler.ServiceOptionKey), "s3");
request.Options.Set(new HttpRequestOptionsKey<string>(SigV4SigningHandler.RegionOptionKey), "us-west-2");

var response = await http.SendAsync(request);

Control how the payload is signed

By default the signer hashes the request body and includes that hash in the signature. For a large or streaming upload, hashing means reading the whole body first. You have two ways to avoid that:

  • Set SignPayload = false on AWSSigV4Parameters. The body is signed as UNSIGNED-PAYLOAD and is never read. This leaves the body out of the signature, so SigV4 no longer detects changes to the payload in transit and you rely on TLS to protect it. For that reason it requires HTTPS.
  • Supply a precomputed hash as an x-amz-content-sha256 header on the signing request. The signer uses it verbatim and does not read the body.

To sign a large body without buffering it, set SignPayload = false:

var parameters = new AWSSigV4Parameters
{
    Credentials = DefaultAWSCredentialsIdentityResolver.GetCredentials(),
    Region = RegionEndpoint.USWest2,
    Service = "s3",
    SignPayload = false, // sign UNSIGNED-PAYLOAD; requires HTTPS
};

Conclusion

In this post, I showed how to use the standalone SigV4 signer to sign any request the SDK’s generated clients do not cover, from IAM-authorized API Gateway calls to presigned URLs you send with your own HTTP stack. You bring the request and the credentials; the signer produces a signature AWS services accept.

To get started, update to AWSSDK.Core version 4.0.101.0 or later and try the signer against one of your IAM-authorized endpoints. For more detail, read the API reference for Amazon.Runtime.Signing in the AWS SDK for .NET documentation.

Have a question or feedback? File issues and feature requests on the aws-sdk-net GitHub repository.

TAGS: ,
Garrett Beatty

Garrett Beatty

Garrett is a software development engineer on the .NET SDK team at AWS. He is working on projects and tools that aim to improve .NET developer’s experience on AWS. You can find him on GitHub @GarrettBeatty and LinkedIn @garrett-beatty