Skip to content

Request Signing

Algorithm and keys

  • Digest: SHA-256
  • Signature: ECDSA ES256 (curve P-256 + SHA-256; signature value is DER, then Base64)
  • Key generation:
bash
openssl ecparam -name prime256v1 -genkey -noout -out priKey.pem
openssl ec -in priKey.pem -pubout -out pubKey.pem
# Recommended: convert to PKCS#8
openssl pkcs8 -topk8 -nocrypt -in priKey.pem -out priKey_pkcs8.pem

Keep the private key for signing; submit the public key to the platform for verification.

Digest

text
Digest = "SHA-256=" + Base64(SHA256(raw_body_bytes))
  • POST / PUT / PATCH: hash the exact raw HTTP body bytes that will be sent (do not re-serialize JSON)
  • GET / DELETE: hash an empty byte string

Canonical Signing String

Exactly these 4 lines, in this order; lowercase field names; one space after the colon; \n (LF) line breaks:

text
(request-target): <lowercase-method> <path>[?<query>]
x-timestamp: <X-Timestamp>
x-nonce: <X-Nonce>
digest: <Digest>

Example:

text
(request-target): post /coin/v1/exchange/quote
x-timestamp: 1710000000
x-nonce: a9f3c1d47e8b9a2c
digest: SHA-256=Base64DigestValue

Authorization

text
hashBytes    = SHA256(UTF8(canonicalString))
signature    = ECDSA_SIGN_DER(privateKey, hashBytes)
signatureB64 = Base64(signature)

Authorization =
  Signature keyId="<X-Merchant-Id>",alg="ES256",headers="(request-target) x-timestamp x-nonce digest",signature="<signatureB64>"

Server verification and replay protection

  1. Verify Digest matches the raw body
  2. Rebuild the Canonical String and verify Authorization.signature
  3. X-Timestamp window: ±300s
  4. X-Nonce anti-replay: the same X-Merchant-Id + X-Nonce must not repeat within 5 minutes