Appearance
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:
- Extract signature from URL parameters
- Filter out empty/null/undefined values
- Canonicalize parameters (sort keys, use raw values)
- Compute HMAC-SHA256 (hex lowercase)
- 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_xxxxxorsk_test_xxxxx) - If no active API key exists or decryption fails, a fallback secret is used (
REDIRECT_SIGNING_FALLBACKenvironment 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(notorder_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