Skip to content

Signing requests

David Lievrouw edited this page Feb 6, 2024 · 17 revisions

This page describes the configuration required to signing requests.

Basics

When signing a request message, an Authorization header is set in a http request. Using this header, the server can verify that it is sent by the known client, and that the content has not been tampered with.

The signing will result in a request header that will look like:

Authorization: Signature keyId="e0e8dcd638334c409e1b88daf821d135",algorithm="hs2019",created=1584806516,expires=1584806576,headers="(request-target) dalion-app-id date digest",nonce="38brRy8BLUajMbUqWumXPg",signature="DUKQVjiirGMMaMOy9qIwKMro46R3BlLsvUQkw1/8sKQ="

Samples are available in the repository source.

Installation

dotnet add package Dalion.HttpMessageSigning.Signing

or

PM> Install-Package Dalion.HttpMessageSigning.Signing

Configuration

Add the following to your DI configuration:

public static void ConfigureServices(IServiceCollection services) {
    services
        ...
        .AddHttpMessageSigning()
        .UseKeyId(new KeyId("e0e8dcd638334c409e1b88daf821d135"))
        .UseSignatureAlgorithm(SignatureAlgorithm.CreateForSigning(hmcaSecret: "yumACY64r%hm"));
}

There are several .Use...() extension methods available. They all configure properties of a SigningSettings object, associated with the specified KeyId.

The KeyId is an opaque string that is used by the server to identify the known client. It should be unique for every client application. The _SigningSettings` provide the ability to configure the way that messages are signed.

Property Required Description Default value
SignatureAlgorithm yes The signature algorithm that is used to perform the signing. See Supported algorithms. [none]
UseDeprecatedAlgorithmParameter no A value indicating whether the 'algorithm' parameter should report deprecated algorithm names, instead of 'hs2019', for backwards compatibility. false
AutomaticallyAddRecommendedHeaders no A value indicating whether to automatically make the headers, that are recommended by the spec, a part of the canonical signing string. When this is set to false, you need to specify at least one header. true
Expires no The timespan after which the signature is considered expired by the server. TimeSpan.FromMinutes(5)
EnableNonce no A value indicating whether a ´Nonce´ value will be added to the request. true
AuthorizationScheme no The name of the authorization scheme to set as scheme of the Authorization header Signature
DigestHashAlgorithm no The hash algorithm that is used to calculate the digest header value. See Digest header. [digest disabled]
RequestTargetEscaping no The method of escaping the value of the (request-target) pseudo-header. Possible methods are RFC3986 (default), RFC2396, Unescaped or OriginalString. RequestTargetEscaping.RFC3986
Headers no The additional headers that should also be included in the signature calculation. Array.Empty<HeaderName>()
Events no Adds the ability to configure callbacks when requests are signed. [none]

Multiple overloads of AddHttpMessageSigning are available. Use the one that suits your needs best.

Logging

Several services have an optional dependency to ILogger<T>, of the Microsoft.Extensions.Logging package. When logging is configured in a consuming application, messages about the signing and verifying of requests will be logged.

This configuration is optional, signing and verifying will not throw an Exception when the logging configuration is missing.

You can wire up a logging framework of your choice in the consuming application.

More information about logging in .NET Core.

Usage

When configured, an IRequestSignerFactory is registered in your composition root. Example usage:

public class SignRequestService {
    private readonly IHttpClient<SignRequestService> _httpClient;
    private readonly IRequestSignerFactory _requestSignerFactory;

    public SignRequestService(IRequestSignerFactory requestSignerFactory, IHttpClient<SignRequestService> httpClient) {
        _requestSignerFactory = requestSignerFactory ?? throw new ArgumentNullException(nameof(requestSignerFactory));
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task<HttpResponseMessage> SendSignedRequest(HttpRequestMessage request, CancellationToken cancellationToken) {
        var requestSigner = _requestSignerFactory.CreateFor(keyId: "e0e8dcd638334c409e1b88daf821d135");
        await requestSigner.Sign(request);
        return await _httpClient.SendAsync(request, cancellationToken);
    }
}

Alternatively, you can use a delegating handler for your HttpClient, instead of manually signing.

Using a custom delegating handler

We recommend using a custom delegating handler in your HttpClient pipeline.

public class SignApiRequestHandler : DelegatingHandler {
    private readonly IRequestSignerFactory _requestSignerFactory;
        
    public SignApiRequestHandler(IRequestSignerFactory requestSignerFactory) {
        _requestSignerFactory = requestSignerFactory ?? throw new ArgumentNullException(nameof(requestSignerFactory));
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
        var requestSigner = _requestSignerFactory.CreateFor(keyId: "f1ed1eff7ca4429abe1abbbe9ae6419a");
        await requestSigner.Sign(request);
        return await base.SendAsync(request, cancellationToken);
    }
}

And wire it up in your composition root:

public static void ConfigureServices(IServiceCollection services) {
    services
        ...
        .AddHttpClient<ApiClient>()
        .AddHttpMessageHandler<SignApiRequestHandler>()
        .Services.AddTransient<SignApiRequestHandler>();
}

For more information about delegating handlers, read this article.

Since version 3.0.0, a delegating handler is included in the Signing package: HttpRequestSigningHandler. You can use that one, or write your own.

Digest header

This library has the ability to verify that the request body has not been tampered with. It uses a Digest header for this. This Digest header is added to the request and is then taken into account when calculating the request signature.

Digest: SHA-256=RA1E8y/pYWwNhV2Emk+DxFNr9m9P4N/SlMsEOBfgsGc=

The SigningSettings contain a DigestHashAlgorithm property. When this is set to default, this feature is disabled.

When set to a valid HashAlgorithmName (e.g. HashAlgorithmName.SHA256), the digest header value will be calculated using the specified algorithm.

Note

When the request to sign already specifies a digest header, the value is not changed or calculated again.

When verifying, the server will perform the same calculation, to verify that the body of the request has not been changed since the request was sent.

This functionality is disabled by default, enable it by setting the DigestHashAlgorithm property of the SigningSettings to a non-default value. Or use the .UseDigestAlgorithm() method during composition.

Nonce value

You have the ability to add a Nonce value. This is a unique value, per signature, that is sent in the Authorization header. It is also taken into account when generating the signature string.

nonce="38brRy8BLUajMbUqWumXPg",signature="DUKQVjiirGMMaMOy9qIwKMro46R3BlLsvUQkw1/8sKQ="

This is used to prevent replay attacks: The server will keep a record of the received nonce values. When a request is received and verified, but the nonce value has already been processed by the server in the past, signature verification will fail. This also means that retries in client code will need to be re-signed.

Because the Nonce value is taken into account when generating the signature string, a malicious party is unable to change the nonce value in the Authorization header. Signature string verification will fail.

You can enable or disable this behavior by setting the EnableNonce property of SigningSettings. Or use the .UseNonce() method during composition.

URI escaping

By default, when creating the signature string, the value of the (request-target) pseudo-header is escaped according to RFC 3986. You can escape the request URI yourself before signing, but that's not a requirement. The signing package can perform escaping for you.

There are several escaping methods available:

  • RFC3986: The default option. The (request-target) will be escaped according to RFC 3986.
  • RFC2396: RFC 2396, obsoleted by RFC 3986.
  • Unescaped: Any escaping of the value of the (request-target) pseudo-header will be removed.
  • OriginalString: This library will not perform any escaping for you, you take matters into your own hands.

Using OriginalString can be useful when signing requests for 3rd party verifiers, that require a specific form of escaping that is not covered by any of the other available options.

Clone this wiki locally