HTTP Request Signing
1. What is HTTP Request Signing?
HTTP Request Signing is an enhanced authentication method that uses asymmetric cryptography (public/private key pairs). Instead of relying solely on a shared API key, you sign each request with your private key, and StraitsX verifies it using your public key. This ensures both the integrity and authenticity of every API request.
2. Do I have to switch to this new authentication method?
No. This is an optional upgrade. Your existing API key authentication (X-XFERS-APP-API-KEY) continues to work as before. You can adopt HTTP Request Signing at your own pace.
3. How do I know if HTTP Request Signing is enabled for my account?
To check your status, log in to your StraitsX Dashboard and navigate to the Public Key management section. If the activation toggle is switched On, HTTP Request Signing is active.
Note:
The toggle can only be enabled if you have at least one active key in your account.
4. Which signing algorithm does StraitsX support?
We are using Ed25519 signing algorithm. Ed25519 keys use asymmetric cryptography (no shared secret), making them more suitable for API integrations where the private key never leaves your server.
Ed25519 also generates deterministic signatures (same output for the same input every time), eliminating the need for a secure random number generator during signing and reducing the risk of implementation errors.
5. How do I generate my key pair?
Method 1: Using OpenSSL (Recommended)
Most Linux, macOS, and Windows (via Git Bash) systems have OpenSSL installed.
Generate the Private Key
Run the following command to create a private key file named
private.pem:openssl genpkey -algorithm ed25519 -out private.pemExtract the Public Key
Now, generate the corresponding public key from that private key:
openssl pkey -in private.pem -pubout -out public.pem
Method 2: Using ssh-keygen
If you prefer using standard SSH tools, you can generate the key pair this way:
Generate the Key Pair
ssh-keygen -t ed25519 -N "" -f ./my_signing_keyThis creates
my_signing_key(Private) andmy_signing_key.pub(Public).Convert Private Key to PEM Format (if needed)
Some libraries require PEM format. To convert the SSH private key:
ssh-keygen -p -m PEM -f ./my_signing_key
Note: You may need to convert keys to PEM format depending on your integration library.
6. How do I upload my public key?
Log in to the StraitsX Dashboard and navigate to the Public Key management section. From there you can upload, view, and manage your public keys.
7. What is the canonical string and how do I build it?
The canonical string is a deterministic representation of your request that both you and StraitsX build independently. It is what gets signed with your Ed25519 private key, and what the server reconstructs to verify your signature.
The signing string is six components joined by newline characters (\n):
METHOD\nPATH\nQUERY\nTIMESTAMP\nNONCE\nBODY
| Component | Description | Example |
|---|---|---|
| METHOD | HTTP method, uppercased | POST, GET, DELETE |
| PATH | Request path only (no query string, no host) | /v1/fx/payouts |
| QUERY | Normalized query string (see below). Empty string if none. | page[number]=2&page[size]=20 |
| TIMESTAMP | Unix timestamp (seconds since epoch), sent in X-TIMESTAMP header | 1640000000 |
| NONCE | Unique random string per request, sent in X-NONCE header | f47ac10b-58cc-4372-a567-0e02b2c3d479 |
| BODY | Raw request body. Empty string for requests without a body (e.g. GET, DELETE) | {"amount":"100"} |
Query String Normalization
Query parameters must be normalized before inclusion in the signing string to ensure both client and server produce an identical canonical string.
Steps:
- Take the raw query string from the URL (everything after
?). - Split on
&to get individualkey=valuepairs. - Sort the pairs alphabetically (lexicographic byte order on the full
key=valuestring). - Rejoin with
&. - If there are no query parameters, use an empty string
"".
Do not decode or re-encode the pairs — sort them exactly as they appear in the URL.
Request: POST /v1/fx/payouts
===START OF CANONICAL STRING===
POST
/v1/fx/payouts
1640000000
f47ac10b-58cc-4372-a567-0e02b2c3d479
{"quoteId":"c4d1da72-111e-4d52-bdbf-2e74a2d803d5"}
===END OF CANONICAL STRING===The QUERY line is an empty string (no query parameters).
Signing the String
Once you’ve built the canonical string:
- Sign it using your Ed25519 private key.
- Base64-encode the resulting signature (strict encoding, no line breaks).
- Send the encoded signature in the
X-SIGNATUREheader.
| Header | Value |
|---|---|
X-XFERS-APP-API-KEY | Your API key |
X-PUBLIC-KEY-ID | ID of the registered public key |
X-TIMESTAMP | Same timestamp used in the signing string |
X-NONCE | Same nonce used in the signing string |
X-SIGNATURE | Base64-encoded Ed25519 signature |
Code Examples:
import time
import uuid
import base64
from urllib.parse import urlparse, parse_qsl, urlencode
from nacl.signing import SigningKey
# Request components
method = "GET"
url = "/v1/fx/payouts?sort=createdAt&page[size]=20"
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
body = ""
# Split path and query, normalize query
parsed = urlparse(url)
path = parsed.path
query = "&".join(sorted(parsed.query.split("&"))) if parsed.query else ""
# Build canonical string
signing_string = f"{method}\n{path}\n{query}\n{timestamp}\n{nonce}\n{body}"
# Sign with Ed25519
signing_key = SigningKey(your_private_key_bytes)
signature = signing_key.sign(signing_string.encode()).signature
encoded_signature = base64.b64encode(signature).decode()import { sign } from 'tweetnacl';
import { Buffer } from 'buffer';
import { v4 as uuidv4 } from 'uuid';
// Request components
const method = 'GET';
const url = '/v1/fx/payouts?sort=createdAt&page[size]=20';
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = uuidv4();
const body = '';
// Split path and query, normalize query
const [path, rawQuery] = url.split('?');
const query = rawQuery ? rawQuery.split('&').sort().join('&') : '';
// Build canonical string
const signingString = [method, path, query, timestamp, nonce, body].join('\n');
// Sign with Ed25519
const signature = sign.detached(
Buffer.from(signingString),
yourPrivateKeyBytes
);
const encodedSignature = Buffer.from(signature).toString('base64');package main
import (
"crypto/ed25519"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/url"
"os"
"sort"
"strings"
"time"
"github.com/google/uuid"
)
func main() {
// Request components
method := "GET"
rawURL := "/v1/fx/payouts?sort=createdAt&page[size]=20"
timestamp := fmt.Sprintf("%d", time.Now().Unix())
nonce := uuid.New().String()
body := ""
// Split path and query, normalize query
parsed, _ := url.Parse(rawURL)
path := parsed.Path
query := ""
if parsed.RawQuery != "" {
pairs := strings.Split(parsed.RawQuery, "&")
sort.Strings(pairs)
query = strings.Join(pairs, "&")
}
// Build canonical string
signingString := strings.Join([]string{method, path, query, timestamp, nonce, body}, "\n")
// Sign with Ed25519
keyData, _ := os.ReadFile("private.pem")
block, _ := pem.Decode(keyData)
parsedKey, _ := x509.ParsePKCS8PrivateKey(block.Bytes)
privateKey := parsedKey.(ed25519.PrivateKey)
signatureBytes := ed25519.Sign(privateKey, []byte(signingString))
encodedSignature := base64.StdEncoding.EncodeToString(signatureBytes)
fmt.Println("Signature:", encodedSignature)
}require "openssl"
require "base64"
require "securerandom"
require "uri"
# Request components
method = "GET"
raw_url = "/v1/fx/payouts?sort=createdAt&page[size]=20"
timestamp = Time.now.to_i.to_s
nonce = SecureRandom.uuid
body = ""
# Split path and query, normalize query
uri = URI.parse(raw_url)
path = uri.path
query = uri.query ? uri.query.split("&").sort.join("&") : ""
# Build canonical string
signing_string = [method, path, query, timestamp, nonce, body].join("\n")
# Sign with Ed25519
private_key = OpenSSL::PKey.read(File.read("private.pem"))
signature_bytes = private_key.sign(nil, signing_string)
encoded_signature = Base64.strict_encode64(signature_bytes)
puts "Signature: #{encoded_signature}"Important Notes:
- Sort the raw URL-encoded pairs. If your URL contains
filter%5BpageSize%5D=20, sort using that encoded form, not the decodedfilter[pageSize]=20.- The PATH component never includes the query string.
/v1/fx/transactionsonly — no?.- The QUERY and BODY components are separate. GET requests always have an empty BODY, regardless of query parameters.
- Duplicate keys are allowed. If the URL has
tag=a&tag=b, both pairs are kept and sorted independently.- The timestamp and nonce in the headers must match exactly what was used in the signing string. Any mismatch will cause verification to fail.
- Each nonce must be unique — reusing a nonce will be rejected as a replay attack.
- The timestamp must be recent — expired timestamps are rejected.
- The body must be the exact raw string sent in the request. Differences in whitespace or key ordering will invalidate the signature.
8. What is the timestamp tolerance?
The server accepts requests with timestamps within ±300 seconds (5 minutes) of the server’s current time. Requests outside this window will be rejected with a clock_skew error. Ensure your server clock is synchronized via NTP.
9. What is a nonce and why is it needed?
A nonce ("number used once") is a unique UUID string included in each request. It prevents replay attacks — if someone intercepts a valid signed request and tries to resend it, the server will detect the duplicate nonce and reject it.
The accepted regex format for this UUID is /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i.
10. What happens if my signature is invalid?
The API will return one of the following error responses:
| Error | Error Code | HTTP Status | Description |
|---|---|---|---|
| Missing Required Signature Headers | STXE-3000 | 400 | One or more required signing headers (X-XFERS-APP-API-KEY, X-PUBLIC-KEY-ID, X-SIGNATURE, X-TIMESTAMP, X-NONCE) are missing. |
| Missing Public Key ID | STXE-3000 | 400 | The X-PUBLIC-KEY-ID header is required when signature authentication is enabled for your account. |
| Invalid Nonce Format | STXE-3000 | 400 | The nonce must be a valid UUID format. |
| Invalid Timestamp Format | STXE-3000 | 400 | The provided timestamp format is invalid. Please use Unix timestamp (seconds since epoch). |
| Public Key Inactive | STXE-4000 | 400 | The specified public key is inactive and cannot be used for signature verification. |
| Invalid Request Signature | STXE-1000 | 401 | The provided signature could not be verified. Ensure you are signing the canonical string correctly with your private key. |
| Request Timestamp Expired | STXE-1000 | 401 | The request timestamp is outside the acceptable time window (±5 minutes). Ensure your system clock is synchronized. |
| Replay Attack Detected | STXE-1000 | 401 | This nonce has already been used. Generate a new unique nonce for each request. |
| Key Ownership Mismatch | STXE-2000 | 403 | The API token and public key must belong to the same user. |
| Public Key Not Found | STXE-5000 | 404 | The specified public key could not be found or has been deactivated. |
11. Can I rotate my keys?
Yes. The system supports multiple public keys identified by X-PUBLIC-KEY-ID. You can upload a new public key, start using it in your requests, and then deactivate or delete the old key — enabling zero-downtime key rotation.
12. Can I switch back to API key authentication?
Yes. HTTP Request Signing is controlled by a per-account configuration. If needed, it can be toggled from the StraitsX Account Dashboard and your existing API key authentication will continue to work as before.
Updated about 22 hours ago
