Skip to content

Signature Verification Algorithm

This document describes the exact algorithm for verifying redirect signatures as specified in the Kore Payment Platform documentation.

Algorithm Overview

The signature verification uses HMAC-SHA256 with the following steps:

  1. Extract signature from URL parameters
  2. Filter out empty/null/undefined values
  3. Canonicalize parameters (sort keys, use raw values)
  4. Compute HMAC-SHA256 (hex lowercase)
  5. Compare signatures (case-sensitive, constant-time)

Step-by-Step Implementation

Step 1: Extract Signature

javascript
const urlParams = new URLSearchParams(window.location.search);
const receivedSignature = urlParams.get('signature');

Step 2: Create Parameter Object (Excluding Signature)

javascript
const params = {};
for (const [key, value] of urlParams.entries()) {
  if (key !== 'signature') {
    params[key] = value;
  }
}

Step 3: Filter Empty Values

javascript
const filteredParams = {};
for (const key in params) {
  const value = params[key];
  if (value !== undefined && value !== null && value !== '') {
    filteredParams[key] = value;
  }
}

Step 4: Canonicalize Parameters

Key Points:

  • Sort all keys alphabetically
  • Use raw values (NO URL encoding)
  • Format: key=value&key2=value2
javascript
function canonicalize(params) {
  const keys = Object.keys(params).sort();
  return keys.map(k => `${k}=${params[k]}`).join('&');
}

const canonical = canonicalize(filteredParams);
// Example: "gateway=checkout&order_id=ORD-123&status=captured"

Important: Do NOT use encodeURIComponent() - use raw values directly.

Step 5: Compute HMAC-SHA256

javascript
async function computeHMAC(message, secret) {
  const encoder = new TextEncoder();
  const keyData = encoder.encode(secret);
  const messageData = encoder.encode(message);
  
  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    keyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  
  const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
  const hashArray = Array.from(new Uint8Array(signature));
  
  // Convert to hex string (lowercase)
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase();
}

const computedSignature = await computeHMAC(canonical, apiSecret);

Step 6: Compare Signatures

javascript
// Use constant-time comparison to prevent timing attacks
function constantTimeEqual(a, b) {
  if (a.length !== b.length) return false;
  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  return result === 0;
}

const isValid = constantTimeEqual(computedSignature, receivedSignature);

Complete Implementation

javascript
async function verifyRedirectSignature(params, apiSecret) {
  // Convert to object if URLSearchParams
  const paramObj = params instanceof URLSearchParams
    ? Object.fromEntries(params.entries())
    : { ...params };
  
  // Extract signature
  const receivedSignature = paramObj.signature;
  if (!receivedSignature) {
    return false;
  }
  
  // Remove signature from params
  const { signature, ...paramsToSign } = paramObj;
  
  // Filter empty values
  const filteredParams = {};
  for (const key in paramsToSign) {
    const value = paramsToSign[key];
    if (value !== undefined && value !== null && value !== '') {
      filteredParams[key] = value;
    }
  }
  
  // Canonicalize: sort keys, use raw values
  const keys = Object.keys(filteredParams).sort();
  const canonical = keys.map(k => `${k}=${filteredParams[k]}`).join('&');
  
  // Compute HMAC-SHA256
  const computedSignature = await computeHMAC(canonical, apiSecret);
  
  // Compare (constant-time)
  return constantTimeEqual(computedSignature, receivedSignature);
}

Secret Key Source

The signature is created using the merchant's API secret key. The backend:

  • Decrypts the secret key from ApiKeySecret.encryptedKey
  • The merchant's API secret key is the same secret key that was generated when the API key was created (e.g., sk_live_xxxxx or sk_test_xxxxx)
  • If no active API key exists or decryption fails, a fallback secret is used (REDIRECT_SIGNING_FALLBACK environment variable or 'temporary_redirect_secret')

For SDK verification: You must use the same secret key that was used to create the signature. This is your merchant's API secret key, which should be stored securely on your server.

Security Notes:

  • CRITICAL: Always verify signatures server-side when possible
  • The merchant's API secret key should never be exposed in client-side code
  • If client-side verification is necessary, fetch the secret from your secure backend endpoint
  • Never commit API secrets to version control or expose them in environment variables accessible to the frontend

Testing Note:

  • If you're testing and the backend is using the fallback secret (temporary_redirect_secret), you'll need to use the same secret for verification
  • In production, always use your actual merchant API secret key for both signing (backend) and verification (SDK)

Important Notes

1. No URL Encoding

  • DO NOT use encodeURIComponent() in canonicalization
  • Use raw parameter values as-is
  • Example: order_id=ORD-123 (not order_id=ORD%2D123)

2. Signature Format

  • Algorithm: HMAC-SHA256
  • Output: Hexadecimal string (lowercase)
  • Example: a1b2c3d4e5f6... (64 characters for SHA-256)

3. Case Sensitivity

  • Parameter values are case-sensitive
  • Signature comparison is case-sensitive
  • Always convert computed signature to lowercase

4. Parameter Order

  • Keys are sorted alphabetically for canonicalization
  • Order in URL doesn't matter
  • Example: gateway=checkout&order_id=123&status=captured

5. Empty Values

  • Filter out: undefined, null, empty string ''
  • Only include non-empty parameters in signature

Common Mistakes

❌ Wrong: URL Encoding

javascript
// WRONG - Don't URL encode
const canonical = keys.map(k => `${k}=${encodeURIComponent(params[k])}`).join('&');

✅ Correct: Raw Values

javascript
// CORRECT - Use raw values
const canonical = keys.map(k => `${k}=${params[k]}`).join('&');

❌ Wrong: Wrong Key Order

javascript
// WRONG - Don't use original order
const canonical = Object.keys(params).map(k => `${k}=${params[k]}`).join('&');

✅ Correct: Sorted Keys

javascript
// CORRECT - Sort keys alphabetically
const keys = Object.keys(params).sort();
const canonical = keys.map(k => `${k}=${params[k]}`).join('&');

❌ Wrong: Case-Insensitive Comparison

javascript
// WRONG - Don't ignore case
const isValid = computedSignature.toLowerCase() === receivedSignature.toLowerCase();

✅ Correct: Case-Sensitive Comparison

javascript
// CORRECT - Case-sensitive comparison
const isValid = constantTimeEqual(computedSignature.toLowerCase(), receivedSignature);

Testing

Test with known values:

javascript
const params = {
  order_id: 'ORD-123',
  status: 'captured',
  gateway: 'checkout',
  signature: 'expected_signature_here'
};

const apiSecret = 'your_api_secret';
const isValid = await verifyRedirectSignature(params, apiSecret);

Debugging

Enable debug mode to see the canonical string:

javascript
const kora = new Kora({
  apiKey: 'pk_test_xxx',
  debug: true
});

// This will log:
// [Signature] Canonical string: gateway=checkout&order_id=ORD-123&status=captured
// [Signature] Computed signature: a1b2c3d4...
// [Signature] Received signature: x1y2z3w4...

References

  • See [[Kore Payment SDK Integration Guide]] for full integration details
  • See [[Troubleshooting Signature Verification]] for common issues

Last Updated: SDK Version: 1.0.0