INFO

Diff for /documentation/tutorials/http_signatures/implementing_http_signatures_v6.md

On this page, you can preview the modified document with the changes.
Note that the links within this diff are most likely broken because they are

  • relative to the original document or

  • point to another document that is not part of the changeset.


Go back to the summary


Signatures v6

To make requests sent to the Upvest Investments API more secure, all sent requests must be signed with a signature algorithm defined in the the IETF draft for HTTP Message Signatures v6.

This tutorial guides you through all 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 v1.3.8 and older uses v6 of algorithm, v1.3.9 and newer versions of signature proxy uses V15 of algorithm.

How HTTP signatures work

HTTP request format

In order to be able to sign a request, the request's format should contain all mandatory headers.

HTTP request body digest

For all requests containing a body, the body digest must be calculated and added as a request header named "digest", as documented in the IETF draft for HTTP Digest Fields. In summary, the digest calculation algorithm as used for the Investment API can be applied as follows:

  • Capture the entire request body's byte stream
  • Calculate the SHA-256 hash of the body content
  • Encode the resulting value using Base64 encoding
  • Add a request header named "digest" with this value: SHA-256={digest value}, where {digest value} is replaced by the result of the previous step

Example

Request bodyContent lengthDigest header value
{"hello": "world"}18SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE= SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
{"key": "value"}16SHA-256=lyTB4g5uPk1/V+0l+dTvsAblCFkNUoyQ2ll/andcE+U= SHA-256=lyTB4g5uPk1/V+0l+dTvsAblCFkNUoyQ2ll/andcE+U=

Signatures

The Investment API uses a strict configuration of the HTTP Message Signatures calculation algorithm and requires particular request components to be included in the signature calculation process.

The request signature is transported via two HTTP headers that must be present in all API requests:

Signature headers

Header nameDescriptionDocumentation
signature-inputMetadata for a message signature generated from components within the HTTP messageLink
signatureMessage signature generated from components within the HTTP messageLink

Signature components

To calculate the signature value for the request, the caller must prepare the signature input string and generate a signature of that input string using one of the supported signing algorithms.

The signature input string consists of several components, each defined as a key-value pair. For each component, the key and the value are separated by a colon followed by a space character.

Example

component-key: component-value

To calculate the signature for a request to the Investment API, use the following list of components:

Component keyDescriptionDocumentationExample
@methodHTTP method of a request message.Link@method: POST
@pathTarget path of the HTTP request message.Link@path: /endpoint
@queryQuery component of the HTTP request message including '?' character. An empty query component (i.e. no query parameters at all, only '?) shouldn't get included.Link@query: ?param=value&foo-bar
authorizationHTTP header with authorization token.
NOTE: This component must be used for all requests except the [access-token-request] (/api/Access-Tokens#get-an-access-token-for-requested-scopes).
Linkauthorization: Bearer {access-token}
content-lengthHTTP header with the byte size of the entire HTTP request body.
NOTE: Only requests with a request body must contain this component.
Linkcontent-length: 15
content-typeHTTP header with media type of the HTTP request body.
NOTE: Only requests with a request body must contain this component.
Linkcontent-type: application/json
digestHTTP header with a checksum of the entire HTTP request body.
NOTE: Only requests with a request body must contain this component.
Linkdigest: SHA-256=6alpwuzt6j/qxb25kIuW6T1vsr57nLFFAXP+VOlhTlg=
idempotency-keyOptional. HTTP header with an idempotency key.Linkidempotency-key: f53e65cc-1243-4ddc-945f-330ade3bce8b
upvest-client-idHTTP header with your tenant IDLinkupvest-client-id: 0df8d466-857d-443f-b411-a1b27b5db42e
NOTE

Please note that the order of the component keys should be the same as the order in the signature.

Metadata of the @signature-params component

The @signature-params component contains signature metadata, consisting of several parts, which are combined by a semicolon (;) as separator. The metadata consists of the following parts:

Example of a full @signature-params component

@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"
ComponentDescriptionExample
List of components keysComponent keys (see 'List of signature components' above) are separated by single spaces " ", enclosed in double quote ", and the entire list of component keys enclosed in brackets (...)("@method" "@path" "@query" "accept" "authorization" "content-length" "content-type" "digest" "idempotency-key" "upvest-client-id")
keyidUnique identifier of the signing key.keyid="8d4997a8-cf7a-4e51-adbb-401656a3e5c2"
createdThe creation date of the request, which is the same value as in the Date header of the request, but in unixtime format.created=1633529659
expiresOptional. The expiration date of the request, which is the same value as in the expires header of the request, but in unixtime format.
NOTE: If you have set a queue at your end AND send us expires metadata in the signature, it is recommended to add some extra time buffer to ensure that the call does not expire before it hits the Investment API. The background is that our API gateway checks whether the value is in the future and rejects the request if not.
expires=1633529664
nonceA random unique value, which is generated for this signature.nonce="o085M4cMgpbicuOL"
NOTE

Although the @signature-params component itself is a mandatory part of the signature input string, it is not included in this list here by convention. This behavior is not explicitly mentioned in the IETF draft, but can be implicitly derived from the examples given there.

How to calculate the signature

After all signature components are defined along with @signature-params component, the following signature header values can be calculated.

Signature header values

Header valueDescriptionFormat
signature-inputThe HTTP header signature-input is a Dictionary Structured Field RFC 8941. It contains the metadata for the HTTP message signature generated from the listed components of the HTTP message (these are defined in the IETF draft for HTTP Message Signatures).
The {signature params value} is equal to the value of the component. @signature-params
sig1={signature params value}

Example signature input

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"
Header valueDescriptionFormat
signatureThe signature HTTP header is a Dictionary Structured field RFC 8941. It contains one message signature generated from the listed components of the HTTP message (these are defined in the IETF draft for HTTP Message Signatures). The{signature value} equals to calculated signature value.
The signature value is enclosed in trailing and leading colon characters :.
sig1=:{signature value}:

Calculating the signature value

To calculate the signature value, take all signature components in the same order as the component keys listed in @signature-params, and add the @signature-params component to this list (even though it is not explicitly listed as a signature component in @signature-params itself). Merge them using the new line character as a delimiter - \n.

Example signature input string

@method: POST
@path: /endpoint
@query: ?a=b
accept: application/json
authorization: Bearer access-token
content-length: 16
content-type: application/json
digest: SHA-256=lyTB4g5uPk1/V+0l+dTvsAblCFkNUoyQ2ll/andcE+U=
idempotency-key: 2133825797664cad
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"

Calculate the signature using the signing key and then encode the resulting signature using Base64 encoding.

NOTE
The input string for the signature must not contain any terminating, leading, or additional spaces or line breaks.

Example signature header

signature: sig1=:MIGIAkIBwgt8M6z9WDdEoUOh/2c5zIQxKHfQalVKjepSGibcG2JD0PJ9FYOD65aq8L2FotNcDvWliJKFrdEwZNJCgMVrx7MCQgG8cMJ3dorHLDwmJpp93CdBRMujBWvIpL+dcVawRpzKXt6ZTNkuPLrHKOkKYRtVRyPrnBuG5T9A71VMGUOJFeo3oA==:

Implementing HTTP signatures

This section uses an example to show how the HTTP signature is implemented.

Prerequisites

Assume we have the following HTTP request:

POST /endpoint?a=b HTTP/1.1
host: server
accept: application/json
authorization: Bearer access-token
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: 2133825797664cad
upvest-client-id: 5ec16164-6173-461d-b90d-116d68f55b40

{"key": "value"}

And we will sign this HTTP request with this private key, identified by a keyid of 8d4997a8-cf7a-4e51-adbb-401656a3e5c2:

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

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

  • authorization and upvest-client-id headers are filled according to the Authentication guide.
  • 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.
  • for POST requests, if the content-length header greater than zero, then the digest header is required
  • (Optional): expires header is set to the date/time after which the response is considered out of date.
  • (Optional): idempotency-key header is set.

1. Calculating the request digest

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

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

The resulting value for our example looks as follows:

SHA-256=lyTB4g5uPk1/V+0l+dTvsAblCFkNUoyQ2ll/andcE+U=

2. Generating 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 )

And we get the following nonce:

nonce="o085M4cMgpbicuOL"
NOTE
Make sure that the `nonce` value is unique for each API call.

3. Defining timestamps

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`.

4. Calculating the signature components

@method: POST
@path: /endpoint
@query: ?a=b
accept: application/json
authorization: Bearer access-token
content-length: 16
content-type: application/json
digest: SHA-256=lyTB4g5uPk1/V+0l+dTvsAblCFkNUoyQ2ll/andcE+U=
idempotency-key: 2133825797664cad
upvest-client-id: 5ec16164-6173-461d-b90d-116d68f55b40

5. Calculating the @signature-params component

@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
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`.

6. Calculating the request signature

Combine the signature components with @signature-params into the signature input string by joining them with newline (\n) characters:

@method: POST
@path: /endpoint
@query: ?a=b
accept: application/json
authorization: Bearer access-token
content-length: 16
content-type: application/json
digest: SHA-256=lyTB4g5uPk1/V+0l+dTvsAblCFkNUoyQ2ll/andcE+U=
idempotency-key: 2133825797664cad
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"

Calculate the signature using the signing key and encode it with Base64 encoding:

:MIGIAkIBwgt8M6z9WDdEoUOh/2c5zIQxKHfQalVKjepSGibcG2JD0PJ9FYOD65aq8L2FotNcDvWliJKFrdEwZNJCgMVrx7MCQgG8cMJ3dorHLDwmJpp93CdBRMujBWvIpL+dcVawRpzKXt6ZTNkuPLrHKOkKYRtVRyPrnBuG5T9A71VMGUOJFeo3oA==:

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

POST /endpoint?a=b HTTP/1.1
host: server
accept: application/json
authorization: Bearer access-token
content-length: 16
content-type: application/json
date: Wed, 06 Oct 2021 16:14:19 CEST
digest: SHA-256=lyTB4g5uPk1/V+0l+dTvsAblCFkNUoyQ2ll/andcE+U=
expires: Wed, 06 Oct 2021 16:14:24 CEST
idempotency-key: 2133825797664cad
signature: sig1=:MIGIAkIBwgt8M6z9WDdEoUOh/2c5zIQxKHfQalVKjepSGibcG2JD0PJ9FYOD65aq8L2FotNcDvWliJKFrdEwZNJCgMVrx7MCQgG8cMJ3dorHLDwmJpp93CdBRMujBWvIpL+dcVawRpzKXt6ZTNkuPLrHKOkKYRtVRyPrnBuG5T9A71VMGUOJFeo3oA==:
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"}

Supported signing key algorithms

For signature verification, the Investment API supports the following algorithms:

ECDSA

The supported version of the ECDSA signing algorithm uses the P-521 curve and SHA-512 hashing link.

Private key generation with password protection as supported by the HTTP signature proxy:

openssl ecparam -genkey -name secp521r1 -outform PEM | openssl ec -aes256 -inform PEM -outform PEM -out ecdsa521.priv

Alternatively, but less secure (and not supported by the HTTP signature proxy), you can generate the private key without encrypting it:

openssl ecparam -genkey -name secp521r1 -outform PEM -out ecdsa521-unencrypted.priv

You can then encrypt it in a separate step after creation:

openssl ec -aes256 -inform PEM -outform PEM -in ecdsa521-unencrypted.priv -out ecdsa521.priv

Public key extraction:

openssl pkey -pubout -in ecdsa521.priv > ecdsa521.pub

ED25519

ED25519 signing algorithm uses the Curve255191 curve and SHA-512 hashing link.

NOTE

Currently, our open-source HTTP Signature Proxy does not support ED25519 signing keys.

Private key generation with password protection:

openssl genpkey -algorithm ed25519 -aes256 -outform PEM -out ed25519.priv

Alternatively, but less secure, you can generate the private key without encrypting it:

openssl genpkey -algorithm ed25519 -outform PEM -out ed25519-unencrypted.priv

You can then encrypt it in a separate step after creation:

openssl pkey -aes256 -inform PEM -outform PEM -in ed25519-unencrypted.priv -out ed25519.priv

Public key extraction:

openssl pkey -pubout -in ed25519.priv > ed25519.pub
NOTE

For MacOS users, install the latest OpenSSL using Homebrew (or other package manager of your choice) to be able to generate a ED25519 key pair. The LibreSSL version that is included in MacOS does not support ED25519.

brew install openssl
/usr/local/opt/openssl@3/bin/openssl version
</div>

Base64 encoding

For all calculations of Base64 values, the Investment API uses Base64 encoding with the standard alphabet and padding as defined in RFC 4648 section 4. To validate your implementation use the following examples:

Examples:

ValueExpected Base64 value
AQQ==
ABQUI=
AB>QUI+
AB?QUI/

Next steps

If you want to upgrade signatures v6 to v15, read this guide.