Skip to content

1. Signing a request

Wesley Donker edited this page Feb 2, 2017 · 1 revision

The following tutorial explains how to sign an HTTP request.

You will only need the Donker.Hmac assembly for this.

Table of contents

  1. Creating a configuration
  2. Working with keys
  3. Creating the signer
  4. Setting up the request
  5. Extracting the data for signing
  6. Creating the signature
  7. Adding the signature to the request

1. Creating a configuration

Before creating or validating a signature can be done, a configuration must first be created.

IHmacConfiguration configuration = new HmacConfiguration
{
    Name = "My awesome configuration",
    UserHeaderName = "X-Auth-User",
    AuthorizationScheme = "HMAC",
    SignatureDataSeparator = "\r\n",
    SignatureEncoding = "UTF-8",
    HmacAlgorithm = "HMACSHA512",
    MaxRequestAge = TimeSpan.FromMinutes(15),
    SignRequestUri = true,
    ValidateContentMd5 = true,
    Headers = new List<string> { "X-Example-Custom-Header" }
};

Name
Optional.

This is the name of the configuration. This is only important if you're managing multiple configurations, but it does not affect signing or validation.

UserHeaderName
Optional.

In order to create a unique signature, a special key will be used for signing. It's possible to have a unique key for different users. If that's something you want, you can use the UserHeaderName property to define the HTTP header containing this user's name. The username header will also be included in the signature itself. You can leave this value null or empty if you're not working with different users.

AuthorizationScheme
Required.

Once the signature has been created, it will be placed in the official Authorization HTTP header of the request. This header will have the following format:

Authorization: <scheme> <data>

When the signature is added to the request, the AuthorizationScheme is placed in the <scheme> part of the header and the signature in the <data> part.

SignatureDataSeparator
Required.

All the elements of the request that are included in the signature will first be place in a single string, separated by this separator, before being hashed.

SignatureEncoding
Required.

The name of the encoding used when converting the signature string and key into bytes before creating the signature hash. See the following page for available encoding types: https://msdn.microsoft.com/en-us/library/system.text.encoding(v=vs.110).aspx

HmacAlgorithm
Required.

The type of algorithm to use when creating the HMAC signature hash. See the following page for available implementations: https://msdn.microsoft.com/en-us/library/kczffhwa(v=vs.110).aspx

MaxRequestAge
Optional, but recommended.

If a maximum request age is configured, the signer will include the official HTTP Date header in the signature. The validator checks this header to see if the request is too old, to avoid possible replay attacks. If the request is older than the specified maximum request age, the request will be considered invalid.

SignRequestUri
Optional, but recommended.

When this property is true, the URI that the request will be sent to will also be included in the signature. This is recommended but optional, since validation will fail if the signed URI is, for example, a proxy address that differs from the URI of the server that receives the request.

ValidateContentMd5
Optional, but recommended.

When this property is true, the signer will include the Content-MD5 header in the signature. The validator will create an MD5 hash of the request body if one is present and validate it against the Content-MD5 header.

Headers
Optional.

You can specify additional headers to include into the signature with this property, if you like. When a signature is created, the values of these headers will be canonicalized and included.

2. Working with keys

In order to create a unique signature, we'll need to use a key that is only known by the client and server. This key will not be sent with the request itself, but will be used when creating the signature. If the client and server use different keys, the signature will differ and authentication will fail.

The key is retrieved during signing using a key repository. This library comes with two very basic key repositories:

  • SingleHmacKeyRepository
  • SingleUserHmacKeyRepository

Both implement the IHmacKeyRepository interface, so they both require a username to be specified when retrieving the key. The difference here is that the SingleHmacKeyRepository will always return the same key, no matter what username is specified, or if it's specified at all. The SingleUserHmacKeyRepository will only return the key if the correct username is specified.

Of course, these are some very basic implementations. It's recommended to create your own repository, one that retrieves the key for a user from a database for example. It doesn't matter, as long as the repository implements the IHmacKeyRepository interface.

IHmacKeyRepository keyRepository = new SingleUserHmacKeyRepository("John Doe", "abc123def456ghi789");

3. Creating the signer

Once you have a configuration and repository, a signer can be created.

IHmacSigner signer = new HmacSigner(configuration, keyRepository);

The signer is responsible for actually creating the signature of the request and everything required to make that possible.

4. Setting up the request

The Content-MD5 header

If the request to sign contains a body and you specified in the configuration that the Content-MD5 hash needs to be validated, the Content-MD5 header must be present with a valid hash. The signer can help with that.

HttpRequest request;

...

if (request.InputStream != null
    && string.IsNullOrEmpty(request.Headers[HmacConstants.ContentMd5HeaderName]))
{
    string md5Hash = signer.CreateBase64Md5Hash(request.InputStream);
    request.Headers[HmacConstants.ContentMd5HeaderName] = md5Hash;
}

In the example above, we first check if a body is present and if the Content-MD5 hash has not already been added to the headers. We then use the signer to create the MD5 hash in base64 format and add it to the request headers.

The Date header

If a maximum request age is configured, the official HTTP Date header must be present with the request.

string dateString = DateTime.UtcNow.ToString(
    HmacConstants.DateHeaderFormat, 
    CultureInfo.GetCultureInfo(HmacConstants.DateHeaderCulture));

request.Headers[HmacConstants.DateHeaderName] = dateString;

The custom username header

If the UserHeaderName was configured in the configuration, you will need to add a username to this configured header. The signer and validator will retrieve the username from this header and use it to retrieve the key from the repository.

request.Headers["X-Auth-User"] = "John Doe";

Additional headers

If any additional headers were specified in the configuration, these need to be included with the request as well.

request.Headers["X-Example-Custom-Header"] = "some example value";

5. Extracting the data for signing

Once the request has been set up you can use the signer to extract the data to include in the signature.

HmacSignatureData signatureData = signer.GetSignatureDataFromHttpRequest(request);

The signer, as well as the validator, both support HttpRequestBase and HttpRequestMessage objects. If you're working with an HttpRequest object instead, you can encapsulate this with an HttpRequestWrapper object, which is a subclass of HttpRequestBase.

The following signature data is extracted from the request:

  • The key to use for signing, which was retrieved from the repository;
  • The HTTP method of the request (GET, POST, etc.);
  • The hash from the official HTTP Content-MD5 header, if this was specified in the configuration;
  • The content type from the official HTTP Content-Type header;
  • The date from the official HTTP Date header, if a maximum request age was configured;
  • The username from the custom user header, if this was configured;
  • The absolute request URI, if this was specified in the configuration;
  • The additional custom headers if those were specified in the configuration.

6. Creating the signature

With the extracted signature data, the actual signature can be created.

string signature = signer.CreateSignature(signatureData);

The signer will first create a string representation of the signature data, by combining the properties into a single string. In doing so, the following canonicalization is performed on these properties, if they are present:

  • The HTTP method has it's leading and trailing whitespace removed and is converted to uppercase;
  • The Content-MD5 value has it's leading and trailing whitespace removed;
  • The Content-Type value has it's leading and trailing whitespace removed and is converted to lowercase;
  • The Date value has it's leading and trailing whitespace removed;
  • The username stays unaltered.

If the request URI is included, it will be canonicalized in the following way:

  • It's leading and trailing whitespace are removed;
  • The scheme and hostname are converted to lowercase;
  • The default port is included if no port was specified (this value is -1 if no default port is known for the scheme).

If additional headers are included in the signature, these will also be canonicalized in the following way:

  • Header names have their leading and trailing whitespace removed and are converted to lowercase;
  • The header values are normalized (whitespace sequences are reduced to a single space character);
  • Duplicate headers are merged into a single header, where the values will be combined, separated by a comma;
  • The header name and value will be joined, separated by a colon;
  • The canonicalized headers will then be sorted ordinally;
  • And finally, they will be joined into a single string, separated by the configured signature data separator.

Finally, all these values are joined in a single string representation, separated by the configured separator, in the following order:

  • HTTP method;
  • Content-MD5;
  • Content-Type;
  • Date;
  • Username;
  • Canonicalized additional headers;
  • Request URI.

As you can see, only the HTTP method and Content-Type are always included in the signature. The rest is only included, depending on the configuration. If something is not included, an empty string will be inserted instead. The less you include, the less reliable the signature becomes.

The following example, based on the example projects included with this library, shows a string representation of the signature.

POST
5tidf6GzDVuY5uWxqhlacw==
application/json
Sun, 04 Dec 2016 15:18:01 GMT
Neo
x-custom-header:Knock knock...
http://localhost:49774/

Finally, the string representation will converted into bytes using the configured encoding and will be hashed using the correct key, which is also converted to bytes using the same encoding. The configured HMAC algorithm will then be used to create the hash and this hash will be converted into a base64 string.

7. Adding the signature to the request

You can add the Authorization header with the signature using the signer.

signer.SetAuthorizationHeader(request, signature);

The following example, based on the example projects included with this library, shows what the final request will look like once the Authorization header is added.

POST http://localhost:49774/ HTTP/1.1
X-Auth-User: Neo
Date: Sun, 04 Dec 2016 15:16:23 GMT
Content-MD5: 5tidf6GzDVuY5uWxqhlacw==
Authorization: HMAC 2z81TcSV6YJaAXlPHeHrQr12OPIdbNOhCMt8UOMUhsutY7TY3E7N/ufd1am1kKdAdFNA/9MaJBWdvsPxgpbSPA==
Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml, application/json
X-Custom-Header: Knock knock...
User-Agent: RestSharp/105.2.3.0
Content-Type: application/json
Host: localhost:49774
Content-Length: 22
Accept-Encoding: gzip, deflate
Connection: Keep-Alive

{"Value":"sunglasses"}

Keep in mind that only a single Authorization header is allowed, so any existing ones will be removed first.

After this, you're finished. You can then send the request to the server.