English
English
Appearance
English
English
Appearance
Complete technical specification for generating HMAC-SHA256 signatures.
Signature generation is a four-step process:
1. Build Canonical Request
↓
2. Hash Canonical Request
↓
3. Create String to Sign
↓
4. Calculate HMAC-SHA256 SignatureThe canonical request is a standardized representation of your HTTP request.
<HTTP_METHOD>
<REQUEST_PATH>
<QUERY_STRING>
<CANONICAL_HEADERS>
<SIGNED_HEADERS>
<BODY_HASH>Note: Each component is separated by a newline (\n). There is NO newline after the body hash.
HTTP method in uppercase.
POST
GET
PUT
DELETE
PATCHThe path component of the URL (without query string).
/api/data
/api/devices/123
/api/usersURL-encoded query parameters (without leading ?). If no query parameters, use empty string.
// URL with query params
const url = 'https://olt.remala.local/api/data?status=active&limit=10';
// In canonical request
const queryString = 'status=active&limit=10'; // ✅ Correct (no leading ?)
// URL without query params
const url2 = 'https://olt.remala.local/api/data';
const queryString2 = ''; // ✅ Empty stringImportant: Query parameters should keep a stable order between signer and verifier.
// ❌ Wrong
const queryString = 'limit=10&status=active';
// ✅ Correct (stable order)
const queryString = 'limit=10&status=active'; // 'limit' comes before 'status'Signed headers in canonical format:
name:value (no spaces around colon)\n (newline)\n)Example:
x-client-type:service
x-date:2025-10-14T03:52:12Z
x-request-id:bf6c5652-9656-4167-b1d4-10c690a72102Rules:
// ✅ Correct
'x-date:2025-10-14T03:52:12Z\n'
// ❌ Wrong
'X-Date: 2025-10-14T03:52:12Z' // uppercase, space after colon
'x-date :2025-10-14T03:52:12Z' // space before colon
'x-date:2025-10-14T03:52:12Z' // missing trailing newlineSemicolon-separated list of signed header names (lowercase, exact server order).
For Service Authentication:
x-date;x-request-id;x-client-typeFor User Authentication:
x-date;x-tenant-id;x-request-id;x-client-typeSHA256 hash of the request body (lowercase hex).
For requests with body:
const body = '{"message": "Hello"}';
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
// 5d41402abc4b2a76b9719d911017c592For requests without body (GET, DELETE):
const body = ''; // Empty string, not null or undefined
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855💡 Empty Body Hash:
The SHA256 hash of an empty string is always:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Use this constant for GET/DELETE requests.
Request:
POST /api/data?filter=active HTTP/1.1
Host: olt.remala.local
X-Date: 2025-10-14T03:52:12Z
X-Request-Id: bf6c5652-9656-4167-b1d4-10c690a72102
X-Client-Type: service
Content-Type: application/json
{"message": "Hello"}Canonical Request:
POST
/api/data
filter=active
x-client-type:service
x-date:2025-10-14T03:52:12Z
x-request-id:bf6c5652-9656-4167-b1d4-10c690a72102
x-date;x-request-id;x-client-type
5d41402abc4b2a76b9719d911017c592TypeScript:
import crypto from 'crypto';
function buildCanonicalRequest(
method: string,
url: string,
headers: Record<string, string>,
body: string,
isUserAuth: boolean = false
): string {
const u = new URL(url);
// 1. Method (uppercase)
const canonicalMethod = method.toUpperCase();
// 2. Path (without query string)
const canonicalPath = u.pathname;
// 3. Query string (without leading ?)
const queryString = u.search.slice(1);
// 4. Canonical headers (server order, lowercase, with trailing newline)
const signedHeaderNames = isUserAuth
? ['x-date', 'x-tenant-id', 'x-request-id', 'x-client-type']
: ['x-date', 'x-request-id', 'x-client-type'];
const canonicalHeaders = signedHeaderNames
.map(name => `${name}:${headers[name]}`)
.join('\n') + '\n'; // ⚠️ Must end with newline
// 5. Signed headers list
const signedHeaders = signedHeaderNames.join(';');
// 6. Body hash
const bodyHash = crypto
.createHash('sha256')
.update(body || '')
.digest('hex');
// Combine all parts
return [
canonicalMethod,
canonicalPath,
queryString,
canonicalHeaders,
signedHeaders,
bodyHash
].join('\n');
}Python:
import hashlib
from urllib.parse import urlparse
def build_canonical_request(
method: str,
url: str,
headers: dict,
body: str = '',
is_user_auth: bool = False
) -> str:
u = urlparse(url)
# 1. Method (uppercase)
canonical_method = method.upper()
# 2. Path (without query string)
canonical_path = u.path
# 3. Query string (without leading ?)
query_string = u.query
# 4. Canonical headers (server order, lowercase, with trailing newline)
signed_header_names = (
['x-date', 'x-tenant-id', 'x-request-id', 'x-client-type']
if is_user_auth
else ['x-date', 'x-request-id', 'x-client-type']
)
canonical_headers = '\n'.join([
f'{name}:{headers[name]}' for name in signed_header_names
]) + '\n' # ⚠️ Must end with newline
# 5. Signed headers list
signed_headers = ';'.join(signed_header_names)
# 6. Body hash
body_hash = hashlib.sha256((body or '').encode()).hexdigest()
# Combine all parts
return '\n'.join([
canonical_method,
canonical_path,
query_string,
canonical_headers,
signed_headers,
body_hash
])Hash the canonical request using SHA256.
const canonicalRequestHash = crypto
.createHash('sha256')
.update(canonicalRequest)
.digest('hex');Example:
// Canonical request (from Step 1)
const canonicalRequest = `POST
/api/data
filter=active
x-client-type:service
x-date:2025-10-14T03:52:12Z
x-request-id:bf6c5652-9656-4167-b1d4-10c690a72102
x-date;x-request-id;x-client-type
5d41402abc4b2a76b9719d911017c592`;
// Hash it
const hash = crypto.createHash('sha256').update(canonicalRequest).digest('hex');
// Output: 7f3a4e8c9d2b1a5e6f8c7d9a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0cCombine algorithm, timestamp, and canonical request hash.
HMAC-SHA256
<TIMESTAMP>
<CANONICAL_REQUEST_HASH>Note: Each component is separated by a newline (\n).
HMAC-SHA256HMAC-SHA256
2025-10-14T03:52:12Z
7f3a4e8c9d2b1a5e6f8c7d9a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0cTypeScript:
function createStringToSign(
timestamp: string,
canonicalRequest: string
): string {
const canonicalHash = crypto
.createHash('sha256')
.update(canonicalRequest)
.digest('hex');
return [
'HMAC-SHA256',
timestamp,
canonicalHash
].join('\n');
}Python:
def create_string_to_sign(timestamp: str, canonical_request: str) -> str:
canonical_hash = hashlib.sha256(canonical_request.encode()).hexdigest()
return '\n'.join([
'HMAC-SHA256',
timestamp,
canonical_hash
])Sign the string to sign using HMAC-SHA256 with your secret key.
signature = HMAC-SHA256(secretKey, stringToSign)TypeScript:
function calculateSignature(
secretKey: string,
stringToSign: string
): string {
return crypto
.createHmac('sha256', secretKey)
.update(stringToSign)
.digest('hex');
}Python:
import hmac
import hashlib
def calculate_signature(secret_key: str, string_to_sign: str) -> str:
return hmac.new(
secret_key.encode(),
string_to_sign.encode(),
hashlib.sha256
).hexdigest()Go:
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func calculateSignature(secretKey, stringToSign string) string {
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(stringToSign))
return hex.EncodeToString(h.Sum(nil))
}TypeScript:
import crypto from 'crypto';
interface RequestData {
method: string;
url: string;
headers: Record<string, string>;
body: string;
secretKey: string;
credentialType: 'service' | 'user';
credentialId: string;
}
function generateSignature(data: RequestData): string {
const u = new URL(data.url);
const path = u.pathname;
const queryString = u.search.slice(1);
// Step 1: Build canonical request
const isUserAuth = data.credentialType === 'user';
const signedHeaderNames = isUserAuth
? ['x-date', 'x-tenant-id', 'x-request-id', 'x-client-type']
: ['x-date', 'x-request-id', 'x-client-type'];
const canonicalHeaders = signedHeaderNames
.map(name => `${name}:${data.headers[name]}`)
.join('\n') + '\n';
const signedHeaders = signedHeaderNames.join(';');
const bodyHash = crypto
.createHash('sha256')
.update(data.body || '')
.digest('hex');
const canonicalRequest = [
data.method.toUpperCase(),
path,
queryString,
canonicalHeaders,
signedHeaders,
bodyHash
].join('\n');
// Step 2: Hash canonical request
const canonicalHash = crypto
.createHash('sha256')
.update(canonicalRequest)
.digest('hex');
// Step 3: Create string to sign
const stringToSign = [
'HMAC-SHA256',
data.headers['x-date'],
canonicalHash
].join('\n');
// Step 4: Calculate signature
return crypto
.createHmac('sha256', data.secretKey)
.update(stringToSign)
.digest('hex');
}
function createAuthorizationHeader(data: RequestData): string {
const signature = generateSignature(data);
const isUserAuth = data.credentialType === 'user';
const signedHeaders = isUserAuth
? 'x-date;x-tenant-id;x-request-id;x-client-type'
: 'x-date;x-request-id;x-client-type';
return `HMAC-SHA256 CredentialType=${data.credentialType}, CredentialId=${data.credentialId}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
}
// Usage Example - Service Auth
const serviceRequest = {
method: 'POST',
url: 'https://olt.remala.local/api/data',
headers: {
'x-date': new Date().toISOString(),
'x-request-id': crypto.randomUUID(),
'x-client-type': 'service'
},
body: JSON.stringify({ message: 'Hello from External System' }),
secretKey: 'your-secret-key-12345',
credentialType: 'service' as const,
credentialId: 'external-system-client'
};
const authHeader = createAuthorizationHeader(serviceRequest);
console.log(authHeader);
// Output: HMAC-SHA256 CredentialType=service, CredentialId=external-system-client, SignedHeaders=x-date;x-request-id;x-client-type, Signature=...// Usage Example - User Auth
const userRequest = {
method: 'GET',
url: 'https://olt.remala.local/api/onu/unconfigured',
headers: {
'x-date': new Date().toISOString(),
'x-request-id': crypto.randomUUID(),
'x-client-type': 'web',
'x-tenant-id': 'isp-telkom'
},
body: '',
secretKey: 'tenant-secret-key-xyz',
credentialType: 'user' as const,
credentialId: 'admin@telkom.com'
};
const authHeaderUser = createAuthorizationHeader(userRequest);
console.log(authHeaderUser);
// Output: HMAC-SHA256 CredentialType=user, CredentialId=admin@telkom.com, SignedHeaders=x-date;x-tenant-id;x-request-id;x-client-type, Signature=...Use these test cases to validate your implementation:
Input:
const testCase1 = {
method: 'POST',
url: 'https://olt.remala.local/api/data',
headers: {
'x-date': '2025-10-14T03:52:12.000Z',
'x-request-id': 'bf6c5652-9656-4167-b1d4-10c690a72102',
'x-client-type': 'service'
},
body: '{"message":"Hello from External System"}',
secretKey: 'test-secret-key-12345',
credentialType: 'service',
credentialId: 'external-system-client'
};Expected Body Hash:
4bf1c7b7f0e73b19e1d3f0d13a4e81d8ef9f98dbedbb8df53c09c59d9f51e0b4Input:
const testCase2 = {
method: 'GET',
url: 'https://olt.remala.local/api/onu/unconfigured',
headers: {
'x-date': '2025-10-14T03:52:12.000Z',
'x-request-id': 'bf6c5652-9656-4167-b1d4-10c690a72102',
'x-client-type': 'web',
'x-tenant-id': 'isp-telkom'
},
body: '',
secretKey: 'tenant-secret-key',
credentialType: 'user',
credentialId: 'admin@telkom.com'
};Expected Body Hash:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Input:
const testCase3 = {
method: 'PUT',
url: 'https://olt.remala.local/api/devices/123?force=true',
headers: {
'x-date': '2025-10-14T03:52:12.000Z',
'x-request-id': 'bf6c5652-9656-4167-b1d4-10c690a72102',
'x-client-type': 'service'
},
body: '{}',
secretKey: 'test-secret-key-12345',
credentialType: 'service',
credentialId: 'external-system-client'
};Expected Body Hash:
44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a// ❌ Wrong
'X-Date:2025-10-14T03:52:12Z'
// ✅ Correct
'x-date:2025-10-14T03:52:12Z'// ❌ Wrong
'x-date;x-request-id;x-client-type'
// ✅ Correct
'x-date;x-request-id;x-client-type'// ❌ Wrong - ignoring query string
const url = '/api/data?status=active';
const path = '/api/data'; // Missing query string component!
// ✅ Correct - include query string as separate component
const path = '/api/data';
const queryString = 'status=active';
// In canonical request: path\nqueryString\n...// ❌ Wrong
const body = null;
const bodyHash = crypto.createHash('sha256').update(body).digest('hex'); // Error!
// ✅ Correct
const body = '';
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');// ❌ Wrong
'2025-10-14 03:52:12'
'2025-10-14T10:52:12+07:00'
// ✅ Correct
'2025-10-14T03:52:12.000Z'// ❌ Wrong - missing trailing newline
const canonicalHeaders = signedHeaders
.map(name => `${name}:${headers[name]}`)
.join('\n');
// ✅ Correct - has trailing newline
const canonicalHeaders = signedHeaders
.map(name => `${name}:${headers[name]}`)
.join('\n') + '\n';// ❌ Wrong
'x-date: 2025-10-14T03:52:12Z'
'x-date :2025-10-14T03:52:12Z'
// ✅ Correct
'x-date:2025-10-14T03:52:12Z'// ❌ Wrong (old format)
Authorization: HMAC-SHA256 Credential=service:external-system-client, ...
// ✅ Correct (new format)
Authorization: HMAC-SHA256 CredentialType=service, CredentialId=external-system-client, ...If your signature doesn't match, debug step by step:
function debugSignature(data: RequestData) {
console.log('=== DEBUG SIGNATURE GENERATION ===\n');
const u = new URL(data.url);
const path = u.pathname;
const queryString = u.search.slice(1);
// 1. Print input
console.log('1. INPUT:');
console.log('Method:', data.method);
console.log('Path:', path);
console.log('Query String:', queryString);
console.log('Headers:', JSON.stringify(data.headers, null, 2));
console.log('Body:', data.body);
console.log('Credential Type:', data.credentialType);
console.log('Credential ID:', data.credentialId);
console.log('Secret Key Length:', data.secretKey.length);
console.log();
// 2. Print body hash
const bodyHash = crypto.createHash('sha256').update(data.body || '').digest('hex');
console.log('2. BODY HASH:');
console.log(bodyHash);
console.log();
// 3. Print canonical request
const isUserAuth = data.credentialType === 'user';
const signedHeaderNames = isUserAuth
? ['x-date', 'x-tenant-id', 'x-request-id', 'x-client-type']
: ['x-date', 'x-request-id', 'x-client-type'];
const canonicalHeaders = signedHeaderNames
.map(name => `${name}:${data.headers[name]}`)
.join('\n') + '\n';
const signedHeaders = signedHeaderNames.join(';');
const canonicalRequest = [
data.method.toUpperCase(),
path,
queryString,
canonicalHeaders,
signedHeaders,
bodyHash
].join('\n');
console.log('3. CANONICAL REQUEST:');
console.log(canonicalRequest);
console.log();
console.log('Canonical Request (escaped):');
console.log(JSON.stringify(canonicalRequest));
console.log();
// 4. Print canonical hash
const canonicalHash = crypto.createHash('sha256').update(canonicalRequest).digest('hex');
console.log('4. CANONICAL HASH:');
console.log(canonicalHash);
console.log();
// 5. Print string to sign
const stringToSign = [
'HMAC-SHA256',
data.headers['x-date'],
canonicalHash
].join('\n');
console.log('5. STRING TO SIGN:');
console.log(stringToSign);
console.log();
console.log('String to Sign (escaped):');
console.log(JSON.stringify(stringToSign));
console.log();
// 6. Print signature
const signature = crypto
.createHmac('sha256', data.secretKey)
.update(stringToSign)
.digest('hex');
console.log('6. SIGNATURE:');
console.log(signature);
console.log();
// 7. Print authorization header
const authHeader = `HMAC-SHA256 CredentialType=${data.credentialType}, CredentialId=${data.credentialId}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
console.log('7. AUTHORIZATION HEADER:');
console.log(authHeader);
console.log();
return signature;
}When debugging signature mismatches:
\n)CredentialType and CredentialId (not Credential=type:id)For repeated requests to the same endpoint, you can optimize by caching static parts:
// Cache canonical request parts that don't change
const staticParts = {
method: 'POST',
path: '/api/data',
credentialType: 'service' as const,
credentialId: 'external-system-client',
secretKey: SECRET_KEY
};
// Only regenerate dynamic parts
function makeRequest(body: string) {
const timestamp = new Date().toISOString();
const requestId = crypto.randomUUID();
const headers = {
'x-date': timestamp,
'x-request-id': requestId,
'x-client-type': 'service'
};
const signature = generateSignature({
...staticParts,
url: 'https://olt.remala.local/api/data',
headers,
body
});
return { headers, signature };
}// ❌ Inefficient - hashing body multiple times
function badExample(body: string) {
const hash1 = crypto.createHash('sha256').update(body).digest('hex');
const hash2 = crypto.createHash('sha256').update(body).digest('hex'); // Duplicate!
return hash1;
}
// ✅ Efficient - hash once and reuse
function goodExample(body: string) {
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
return bodyHash;
}// ✅ Good - constant-time comparison
function verifySignature(expected: string, actual: string): boolean {
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(actual)
);
}
// ❌ Bad - vulnerable to timing attacks
function badVerifySignature(expected: string, actual: string): boolean {
return expected === actual;
}