Implementing HTTP Message Signatures v6

WARNING

For signing HTTP requests to the Upvest Investment API, please refer to the v15 version instead of this document.

The older v6 version of signatures, as detailed in this document, is only relevant for verifying webhooks that Upvest sends to your systems.

(Some early adopters are grandfathered and still can use this version for API calls.)


The security of the requests sent to the Upvest Investment API is ensured by the fact that all requests are signed with a signature algorithm that is defined in the IETF draft for HTTP Message Signatures v6.

This tutorial guides you through the necessary steps to implement HTTP signatures.

NOTE

You can also use our open source HTTP Signature Proxy to immediately include HTTP signature functionality in an API testing tool of your choice.

HTTP signature proxy up to v1.3.8 uses version v6 of the algorithm, v1.3.9 and newer versions of HTTP signature proxy use version v15 of the algorithm.


Prerequisites

 ✓   Understand HTTP Requests in your programming language.
Details

Most programming languages and libraries have a notion of a request, whether that be an object, structure or a function. You will need to have a mechanism to modify the request prior to it being sent to over the network to Upvest.

Make sure you are familiar with how this can be done in your software stack before proceeding.

It is important that the body and headers of the HTTP request are not modified in transit after you have signed the request. This kind of post-hoc modification is one of the cases the HTTP Message signature is designed to prevent, and your requests will fail if this occurs.



Let's get started!

Although there are lots of details to be aware of, the actual process of creating an HTTP Message Signature is relatively simple.

In this tutorial, we'll walk you through an example of this process with example values. We'll provide links to our conceptual documentation as we go.

   Check your request is correctly formatted.

To be able to sign a request, the format of the request must contain all of the mandatory headers required by Upvest. You can find a list of them here: mandatory headers. If your request has a payload in the body, check that it is formatted correctly.

In order for a request to be correct, we assume the following requirements:

  • The authorization and upvest-client-id headers are filled according to the 'Authentication' standards of the Investment API.

  • The content-length and content-type headers are set based on availability of the request body.

  • accept is set to one of the supported values according to the API specification.

  • date is set to the date and time the message was created.

Some endpoints also require the idempotency-key to be set.

You may optionally also include the expires header. This will set the date/time after which the response is considered out of date.

For the purposes of this tutorial we'll assume we have the following HTTP request:

POST /endpoint?a=b HTTP/1.1
host: server
accept: application/json
authorization: Bearer the-OAuth2-access-token-goes-here
content-Length: 16
content-Type: application/json
date: Wed, 06 Oct 2021 16:14:19 CEST
expires: Wed, 06 Oct 2021 16:14:24 CEST
idempotency-key: 424e8603-f12c-4a58-8eb1-5edfe471f3ab
upvest-client-id: 5ec16164-6173-461d-b90d-116d68f55b40

{"key": "value"}

   Create the digest header, if necessary.

This step is required only for requests that contain a request body.

You'll need to:

  • create a SHA256 hash of the request body
  • encode that as a base64 string
  • prefix the encoded value with SHA-256=.

Consider the pseudo-code:

digest = "SHA-256=" + base64(sha256(request.body))

The resulting value for our example looks as follows:

digest = "SHA-256=6alpwuzt6j/qxb25kIuW6T1vsr57nLFFAXP+VOlhTlg="

So our request now looks like this:

POST /endpoint?a=b HTTP/1.1
host: server
accept: application/json
authorization: Bearer the-OAuth2-access-token-goes-here
content-length: 16
digest = "SHA-256=6alpwuzt6j/qxb25kIuW6T1vsr57nLFFAXP+VOlhTlg="
content-Type: application/json
date: Wed, 06 Oct 2021 16:14:19 CEST
expires: Wed, 06 Oct 2021 16:14:24 CEST
idempotency-key: 424e8603-f12c-4a58-8eb1-5edfe471f3ab
upvest-client-id: 5ec16164-6173-461d-b90d-116d68f55b40
upvest-signature-version: 15

{"key": "value"}

For further technical details see: Calculate the digest of an HTTP request..

   Generate a random request nonce.

We use a simple random method that gives us 16 random characters from a specified alphabet:

nonce = random("A-Za-z0-9", 16)

We get the following nonce:

nonce = "o085M4cMgpbicuOL"
NOTE

Make sure that the nonce value is unique for each API call.


   Define timestamps.

We'll need to make a timestamp for the creation of the request. This timestamp will be included in the signature input and metadata later. Optionally, we can also create an expiration timestamp.

created = now().ToUnixTime()
expires = now().add(expiration_duration).ToUnixTime()

created = 1633529659
expires = 1633529664
NOTE

expires is optional but we strongly recommend using it and setting it to a few seconds after created.


   Identify the correct key-pair.

To create a cryptographic signature, we'll also need a public/private key-pair. You should have generated this in the Getting Started Tutorial, and Upvest should already have a copy of the the public key. In the following steps you'll need to use the unique identifier for that public key so that Upvest can correctly identify which key to use to validate your HTTP requests.

For the purposes of this walk-through we'll use a public key identified with the keyid of 8d4997a8-cf7a-4e51-adbb-401656a3e5c2. In this context we'll also share the private key so you can reproduce the same results yourself.

-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIB6l0asXau1p6aSOKHTIrVvCEFT6aIhbw99mbbDeEuwOq0MdZ75InO
1ElIh6w+q/acHXYdGH5JCDzPlj5qku9vI+qgBwYFK4EEACOhgYkDgYYABADiKUUw
QsnlgdjuAT2xSEKmQbv2oZV8kbb/Bc4xhIj1K3HyLBaaTSX5gdnRlqk24WkPeMIX
OkAtopnCjoz4ekO1UwCdEdd2ZcODDUXxbSppXDS/ewFVU+FntEckSdi4RXMq7AC5
avwt+3mpug6ydS+tUDjN58MBR4YPCe9HKwubxJFXdA==
-----END EC PRIVATE KEY-----
WARNING

In reality you should never share your private key. Cryptographic keys should be stored and communicated within the security and compliance rules of your company.


   Calculate the @signature-params component

Now we are ready to create the @signature-parms string. This string lists the headers, and other variable to be included in the signature. They keyid, created and expires are those from the previous steps. In our example it will look like this:

@signature-params: ("@method" "@path" "@query" "accept" "authorization" "content-length" "content-type" "digest" "idempotency-key" "upvest-client-id");keyid="8d4997a8-cf7a-4e51-adbb-401656a3e5c2";created=1633529659;expires=1633529664;nonce="o085M4cMgpbicuOL"
NOTE

If you don't wish to use an expires timestamp, simply omit it, and the ; used to separate key/value pairs.

You may also omit the digest if you have a content-length of zero (i.e. for a GET request).


For further technical information see: Calculate the HTTP Message Signature (v6).

   Construct the signature components.

We construct a string based on the headers and associated values we have created so far. This string won't be directly placed into the HTTP headers, but rather used in the calculation of the signature input.

Please note that the keys for calculating the signature components are not quoted.

Note also that the "digest" header and value will be omitted if the "content-length" is 0 (ie. for GET requests).

@method: POST
@path: /endpoint
@query: ?a=b
accept: application/json
authorization: Bearer the-OAuth2-access-token-goes-here
content-length: 16
content-type: application/json
digest: SHA-256=6alpwuzt6j/qxb25kIuW6T1vsr57nLFFAXP+VOlhTlg=
idempotency-key: 424e8603-f12c-4a58-8eb1-5edfe471f3ab
upvest-client-id: 5ec16164-6173-461d-b90d-116d68f55b40
NOTE

Make sure that the order of the signature components is the same as the order of their keys as they are mentioned in @signature-params.


For further technical information see: Calculate the HTTP Message Signature (v6).

   Calculate the request signature.

Finally it's time to construct the final input for the signature, and then calculate the signature iteslf.

First, we combine the signature components with the @signature-params string, joining them with line breaks (\n):

@method: POST
@path: /endpoint
@query: ?a=b
accept: application/json
authorization: Bearer the-OAuth2-access-token-goes-here
content-length: 16
content-type: application/json
digest: SHA-256=6alpwuzt6j/qxb25kIuW6T1vsr57nLFFAXP+VOlhTlg=
idempotency-key: 424e8603-f12c-4a58-8eb1-5edfe471f3ab
upvest-client-id: 5ec16164-6173-461d-b90d-116d68f55b40
@signature-params: ("@method" "@path" "@query" "accept" "authorization" "content-length" "content-type" "digest" "idempotency-key" "upvest-client-id");keyid="8d4997a8-cf7a-4e51-adbb-401656a3e5c2";created=1633529659;expires=1633529664;nonce="o085M4cMgpbicuOL"

Then we'll calculate the cryptograhic signature of that string. We'll use the private signing key we identified earlier. We'll take that output (commonly expressed a byte array) and encode it with Base64 encoding.

For further information on our supported signing algorithms see: Supported signing key algorithms

In our example, this gives us an output that looks like this;

MIGHAkIAoTnL0VRsu66l+nb91Dfhpq+Fr88fdiy+FgkuYjRjQh0IFROEUEjFQOj5tPu+Ms5Z4llhWhGSw602ZivIZWwum8gCQWPUTjp9zAT8KgkH1Dynxw0nmYHZPAOaLKT2mGZ1YI/o6OjBVy5RkdGVw80IWc0QM3XXeoyH7A+EKdJ2wvUAvBQp

For further technical information see: Calculate the HTTP Message Signature (v6).

   Add the signature and signature-input headers to the request.

Finally we have to pack our signature information back into the request headers. We'll add two headers: signature-input and signature.

To construct the signature-input header we can take the payload from the @signature-params value and prepend sig1=. In our example that means it will look like this:

signature-input: sig1=("@method" "@path" "@query" "accept" "authorization" "content-length" "content-type" "digest" "idempotency-key" "upvest-client-id");keyid="8d4997a8-cf7a-4e51-adbb-401656a3e5c2";created=1633529659;expires=1633529664;nonce="o085M4cMgpbicuOL"

The value of the signature header is the base64 encoded string we encoded in the previous step.

This leaves us with ah HTTP request that looks like this.

POST /endpoint?a=b HTTP/1.1
host: server
accept: application/json
authorization: Bearer the-OAuth2-access-token-goes-here
content-length: 16
content-type: application/json
date: Wed, 06 Oct 2021 16:14:19 CEST
digest: SHA-256=6alpwuzt6j/qxb25kIuW6T1vsr57nLFFAXP+VOlhTlg=
expires: Wed, 06 Oct 2021 16:14:24 CEST
idempotency-key: 424e8603-f12c-4a58-8eb1-5edfe471f3ab
signature: sig1=:MIGHAkIAoTnL0VRsu66l+nb91Dfhpq+Fr88fdiy+FgkuYjRjQh0IFROEUEjFQOj5tPu+Ms5Z4llhWhGSw602ZivIZWwum8gCQWPUTjp9zAT8KgkH1Dynxw0nmYHZPAOaLKT2mGZ1YI/o6OjBVy5RkdGVw80IWc0QM3XXeoyH7A+EKdJ2wvUAvBQp:
signature-input: sig1=("@method" "@path" "@query" "accept" "authorization" "content-length" "content-type" "digest" "idempotency-key" "upvest-client-id");keyid="8d4997a8-cf7a-4e51-adbb-401656a3e5c2";created=1633529659;expires=1633529664;nonce="o085M4cMgpbicuOL"
upvest-client-id: 5ec16164-6173-461d-b90d-116d68f55b40

{"key": "value"}

   HTTP Message Signature complete

Congratulations! If you've read and understood this far than you should now be able to implement HTTP Message signing, for requests to the Upvest Investment API, within your application.