Skip to content

Request Signing ​

How the Cake Mobile SDK signs its requests to the eKYC and FaceAuthen backends: HMAC-SHA256 over a timestamp and the request body, using the per-transaction JWT token as the secret key. This is a different scheme from the backend's ATV <ACCESS_ID>:<SIGNATURE> header. On FaceAuthen tenants with the device signature check enabled, the SDK also signs with a registered device key — see Device Signature.

Why It Exists ​

WhatDescription
AuthenticationVerifies the request originates from an authorized client.
IntegrityEnsures the request body was not tampered with in transit.
Replay preventionThe timestamp bounds how long a captured request stays usable.

Signing Inputs ​

ParamTypeRequiredDefaultDescription
timestampstringYes—RFC3339 timestamp, e.g. 2019-04-21T18:00:15+07:00. Sent in the X-TV-Timestamp header.
requestBodystringYes—The raw request body. Works for JSON and for multipart form data.
jwtTokenstringYes—The token from Create Transaction, used as the HMAC secret key.

WARNING

The JWT is the signing key, so it is a credential, not just an identifier. Never log it, never persist it beyond the transaction, and never send it to a third party.

Algorithm ​

text
StringToSign = timestamp + "\n" + hex_encode(SHA256(requestBody))
Signature    = Base64(HmacSha256(JWTToken, Utf8EncodingOf(StringToSign)))
  1. Hash the raw request body with SHA256 and hex-encode it.
  2. Join the timestamp and that hash with a newline.
  3. UTF-8 encode the result.
  4. Apply HMAC-SHA256 using the JWT token as the key.
  5. Base64 encode the digest.

JavaScript ​

javascript
function signRequest(requestBody, jwtToken) {
  // Generate RFC3339 timestamp
  const date = new Date();
  const timestamp = date.toISOString();

  // Generate hashed body
  const hashedBody = CryptoJS.SHA256(requestBody).toString(CryptoJS.enc.Hex);

  // Create string to sign
  const stringToSign = `${timestamp}\n${hashedBody}`;

  // Generate HMAC-SHA256 signature
  const hmac = CryptoJS.HmacSHA256(stringToSign, jwtToken);

  // Convert to hex and then Base64 encode
  const hexHash = hmac.toString(CryptoJS.enc.Hex);
  const signature = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(hexHash));

  return {
    timestamp: timestamp,
    signature: signature
  };
}

Go ​

go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "time"
)

func signRequest(requestBody, jwtToken string) (string, string, error) {
    // Generate RFC3339 timestamp
    timestamp := time.Now().Format(time.RFC3339)

    // Generate hashed body
    hashFunc := sha256.New()
    if _, err := hashFunc.Write([]byte(requestBody)); err != nil {
        return "", "", err
    }
    hashedBody := hex.EncodeToString(hashFunc.Sum(nil))

    // Create string to sign
    stringToSign := timestamp + "\n" + hashedBody

    // Generate HMAC-SHA256 signature
    h := hmac.New(sha256.New, []byte(jwtToken))
    h.Write([]byte(stringToSign))
    signature := base64.StdEncoding.EncodeToString(h.Sum(nil))

    return timestamp, signature, nil
}

Request Headers ​

HeaderRequiredDescription
X-TV-TimestampYesThe RFC3339 timestamp used in StringToSign.
X-SignatureYesThe Base64-encoded signature.
AuthorizationYesBearer {jwt_token}.
http
POST /api/face-auth HTTP/1.1
Host: <HOSTNAME>
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
X-TV-Timestamp: <TIMESTAMP>
X-Signature: <SIGNATURE>

{
    "cus_user_id": "user_00000001",
    "ref_id": "ref_00000001"
}

Server-Side Verification ​

The backend verifies each request by:

  1. Extracting X-TV-Timestamp and X-Signature from the request.
  2. Recreating the expected signature with the same algorithm.
  3. Comparing with a constant-time comparison, to avoid leaking information through timing.
  4. Proceeding only if the signatures match.

Device Signature ​

On a tenant with the device signature check enabled, the Mobile SDK also signs each request it sends with the transaction token, using the device key registered through Init Signing Credential and Confirm Signing Credential. It adds three headers:

HeaderRequiredDescription
Device-IdWith the signature headersThe device_id the credential was registered for. Must exactly equal the credential's stored device_id (case-sensitive).
X-Device-Key-IdSee belowThe signing key ID — the key_id from Init and Confirm. The credential must be ACTIVE.
X-Device-SignatureSee belowCompact JWS (typ: cake-request-proof+jwt, alg: ES256) produced by the SDK over the environment audience, the method, the URL path as documented (e.g. /v1/face_auth, no gateway prefix), Content-Type, body hash, token hash, issue time and a unique jti.
http
POST /v1/face_auth HTTP/1.1
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
Device-Id: <DEVICE_ID>
X-Device-Key-Id: <DEVICE_KEY_ID>
X-Device-Signature: <DEVICE_SIGNATURE>

The headers are required when the Device-Id sent has an ACTIVE signing credential, or when the SDK version is at or above the version from which signing is enforced. They apply to every SDK request with the transaction token: Verify Face Authen (POST /v1/face_auth), Upload Image (POST /v1/images, signed over the raw multipart body) and POST /v1/get_config.

  • The request is rejected with access_denied_exception (401100) when the signature is missing, invalid, stale or replayed, or when the credential named by X-Device-Key-Id is not ACTIVE or was registered for a different Device-Id.
  • Once a minimum SDK version is set, a request from an older SDK, or without an Sdk-Version header, is rejected with sdk_version_unsupported (2009005).
  • A body over the size limit is rejected with invalid_parameter_exception (4001000) when a signature is required.

WARNING

The device signature protects the request in transit; it does not make the app's view of a result trustworthy. Confirm every authentication outcome from your backend with Verify Transaction Authentication Key.

Next Steps ​

Developers