Signatures v15
The security of the requests sent to the Upvest Investments 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 v15.
This tutorial guides you through the necessary steps to implement HTTP signatures.
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
How to enable v15
To be able to make a request with v15 of signing, add the following version header:
Example version header
X-Signing-Proxy-Version: 15
HTTP request format
To be able to sign a request, the format of the request must contain all mandatory headers.
HTTP request body content-digest
For all requests that contain a body, the body content-digest must be calculated and added as a request header named content-digest
, as described 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-512 hash of the body content.
- Encode the resulting value using Base64 encoding
- Add a request header named
content-digest
with this value:sha-512=:{digest value}:
, where{digest value}
is replaced by the result of the previous step.
Example
Request body | Content length | content-digest header value |
---|---|---|
{"hello": "world"} | 18 | "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==: |
{"key": "value"} | 16 | sha-512=:Hd9/AvGZkbjitW1+Ml8Fg1ux1mtcDYe6mLQjDyoowIWa3LM/PmwN2v9O+MjtQGrCA3EQWUL54dlgxKHyYbrucw==: |
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 name | Description | Documentation |
---|---|---|
signature-input | Metadata for a message signature generated from components within the HTTP message | Link |
signature | Message signature generated from components within the HTTP message | Link |
Signature components
To calculate the signature value for the request, the caller must prepare the signature input string and generate a signature of this input string with one of the supported signing algorithms.
The signature input string consists of several components, each of which is defined as a key-value pair. For each component, the key and the value are separated by a colon followed by a space.
Example
component-key: component-value
To calculate the signature for a request to the Investment API, use the following list of components:
Component key | Description | Documentation | Example |
---|---|---|---|
@method |
HTTP method of a request message. | Link | @method: POST |
@path |
Target path of the HTTP request message. | Link | @path: /endpoint |
@query |
Query component of the HTTP request message including '?' character. | Link | @query: ?param=value&foo-bar |
authorization |
HTTP header with authentication 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). |
Link | authentication: Bearer {access-token} |
content-length |
HTTP header with the byte size of the entire HTTP request body. NOTE: Only requests with a request body must contain this component. |
Link | content-length: 15 |
content-type |
HTTP header with media type of the HTTP request body. NOTE: Only requests with a request body must contain this component. |
Link | content-type: application/json |
content-digest |
HTTP header with a checksum of the entire HTTP request body. NOTE: Only requests with a request body must contain this component. |
Link | content-digest: sha-512=:Hd9/AvGZkbjitW1+Ml8Fg1ux1mtcDYe6mLQjDyoowIWa3LM/PmwN2v9O+MjtQGrCA3EQWUL54dlgxKHyYbrucw==: |
idempotency-key |
Optional. HTTP header with an idempotency key. | Link | idempotency-key:f53e65cc-1243-4ddc-945f-330ade3bce8b |
upvest-client-id |
HTTP header with your tenant ID. | Link | upvest-client-id:0df8d466-857d-443f-b411-a1b27b5db42e |
Please note that the order of the component keys should be the same as the order in the signature.
@signature-params
component
Metadata of the The component @signature-params
contains signature metadata. Since they consist of several parts, they are connected by a semicolon (;
) as a separator. The metadata consists of the following parts:
Example
@signature-params: ("@method" "@path" "@query" "accept" "authorization" "content-length" "content-type" "content-digest" "idempotency-key" "upvest-client-id"); keyid = "8d4997a8-cf7a-4e51-adbb-401656a3e5c2"; created = 1633529659; expires = 1633529664; nonce = "o085M4cMgpbicuOL"
Component | Description | Example |
---|---|---|
List of components keys | Component 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") |
keyid |
Unique identifier of the signing key. | keyid="8d4997a8-cf7a-4e51-adbb-401656a3e5c2" |
created |
The creation date of the request, which is the same value as in the Date header of the request, but in unixtime format. |
created=1633529659 |
expires |
Optional. The expiration date of the request, which is the same value as in the Expires header of the request, but in unixtime format. |
expires=1633529664 |
nonce |
A random unique value, which is generated for this signature. | nonce="o085M4cMgpbicuOL" |
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 the @signature-params
component, the following signature header values can be calculated.
Signature header values
Header value | Description | Format |
---|---|---|
signature-input |
This HTTP header 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 (which 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" "content-digest" "idempotency-key" "upvest-client-id"); keyid ="8d4997a8-cf7a-4e51-adbb-401656a3e5c2"; created = 1633529659; expires = 1633529664;nonce = "o085M4cMgpbicuOL"
Header value | Description | Format |
---|---|---|
signature |
This HTTP header is a dictionary structured field RFC 8941. It contains one message signature generated from the listed components of the HTTP message (which are defined in the IETF draft for HTTP message signatures). The{signature value} is equal to the calculated signature value.The signature value is enclosed by the terminating and leading colons : . |
sig1=:{signature value}: |
Calculating the signature value
To calculate the signature value, proceed as follows:
-
Take all signature components in the same order as the component keys listed in
@signature-params
. -
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
"content-digest": sha-512=:Hd9/AvGZkbjitW1+Ml8Fg1ux1mtcDYe6mLQjDyoowIWa3LM/PmwN2v9O+MjtQGrCA3EQWUL54dlgxKHyYbrucw==:
"idempotency-key": 2133825797664cad
"upvest-client-id": 5ec16164-6173-461d-b90d-116d68f55b40
"@signature-params": ("@method" "@path" "@query" "accept" "authorization" "content-length" "content-type" "content-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 by using Base64 encoding.
Example signature header
signature: sig1=:MIGIAkIBwgt8M6z9WDdEoUOh/2c5zIQxKHfQalVKjepSGibcG2JD0PJ9FYOD65aq8L2FotNcDvWliJKFrdEwZNJCgMVrx7MCQgG8cMJ3dorHLDwmJpp93CdBRMujBWvIpL+dcVawRpzKXt6ZTNkuPLrHKOkKYRtVRyPrnBuG5T9A71VMGUOJFeo3oA==:
Implementing HTTP signatures
After you have calculated the signature, you will learn how to implement it in this section.
Note that here too, the individual steps are illustrated and explained with the help of an example, which is not generally valid.
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"}
We 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:
- The
authentication
andupvest-client-id
headers are filled according to the 'Authentication' guide. - The
content-length
andcontent-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.- Optional: The
expires
header is set to the date/time after which the response is considered out of date. - Optional: The
idempotency-key
header is set.
1. Calculating the request digest
This step is required only for requests that contain a request body.
digest = "sha-512=:" + base64( sha512( request.body ) ) + ":"
The resulting value for our example looks as follows:
sha-512=:Hd9/AvGZkbjitW1+Ml8Fg1ux1mtcDYe6mLQjDyoowIWa3LM/PmwN2v9O+MjtQGrCA3EQWUL54dlgxKHyYbrucw==:
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 )
We get the following nonce:
nonce="o085M4cMgpbicuOL"
3. Defining timestamps
created= now().ToUnixTime()
expires= now().add(expiration_duration).ToUnixTime()
created = 1633529659
expires =1633529664
4. Calculating the signature components
Please, note that the keys for calculation the signature components are quoted.
"@method": POST
"@path": /endpoint
"@query": ?a=b
"accept": application/json
"authorization": Bearer access-token
"content-length": 16
"content-type": application/json
"content-digest": sha-512=:Hd9/AvGZkbjitW1+Ml8Fg1ux1mtcDYe6mLQjDyoowIWa3LM/PmwN2v9O+MjtQGrCA3EQWUL54dlgxKHyYbrucw==:
"idempotency-key": 2133825797664cad
"upvest-client-id": 5ec16164-6173-461d-b90d-116d68f55b40
@signature-params
component
5. Calculating the "@signature-params": ("@method" "@path" "@query" "accept" "authorization" "content-length" "content-type" "content-digest" "idempotency-key" "upvest-client-id");keyid="8d4997a8-cf7a-4e51-adbb-401656a3e5c2";created=1633529659;expires=1633529664;nonce="o085M4cMgpbicuOL"
6. Calculating the request signature
- Combine the signature components with
@signature-params
in the signature input string by joining them with line breaks (\n
):
"@method": POST
"@path": /endpoint
"@query": ?a=b
"accept": application/json
"authorization": Bearer access-token
"content-length": 16
"content-type": application/json
"content-digest": sha-512=:Hd9/AvGZkbjitW1+Ml8Fg1ux1mtcDYe6mLQjDyoowIWa3LM/PmwN2v9O+MjtQGrCA3EQWUL54dlgxKHyYbrucw==:
"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:
:MIGHAkIAoTnL0VRsu66l+nb91Dfhpq+Fr88fdiy+FgkuYjRjQh0IFROEUEjFQOj5tPu+Ms5Z4llhWhGSw602ZivIZWwum8gCQWPUTjp9zAT8KgkH1Dynxw0nmYHZPAOaLKT2mGZ1YI/o6OjBVy5RkdGVw80IWc0QM3XXeoyH7A+EKdJ2wvUAvBQp:
signature
and signature-input
headers to the request
7. Add the 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
content-digest: sha-512=:Hd9/AvGZkbjitW1+Ml8Fg1ux1mtcDYe6mLQjDyoowIWa3LM/PmwN2v9O+MjtQGrCA3EQWUL54dlgxKHyYbrucw==:
expires: Wed, 06 Oct 2021 16:14:24 CEST
idempotency-key: 2133825797664cad
signature: sig1=:MIGHAkIAoTnL0VRsu66l+nb91Dfhpq+Fr88fdiy+FgkuYjRjQh0IFROEUEjFQOj5tPu+Ms5Z4llhWhGSw602ZivIZWwum8gCQWPUTjp9zAT8KgkH1Dynxw0nmYHZPAOaLKT2mGZ1YI/o6OjBVy5RkdGVw80IWc0QM3XXeoyH7A+EKdJ2wvUAvBQp:
signature-input: sig1=("@method" "@path" "@query" "accept" "authorization" "content-length" "content-type" "content-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 the 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:
|
Alternatively, but less secure (and not supported by the HTTP signature proxy), you can generate the private key without encrypting it:
You can then encrypt it in a separate step after creation:
Public key extraction:
ED25519
ED25519 signing algorithm uses the Curve255191
curve and SHA-512
hashing link.
Currently, our open-source HTTP Signature Proxy does not support ED25519 signing keys.
Private key generation with password protection:
Alternatively, but less secure, you can generate the private key without encrypting it:
You can then encrypt it in a separate step after creation:
Public key extraction:
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.
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:
Value | Expected Base64 value |
---|---|
A | QQ== |
AB | QUI= |
AB> | QUI+ |
AB? | QUI/ |
Was this page helpful?