Skip to content

Signature Generation Reference ​

Complete technical specification for generating HMAC-SHA256 signatures.

Overview ​

Signature generation is a four-step process:

1. Build Canonical Request
   ↓
2. Hash Canonical Request
   ↓
3. Create String to Sign
   ↓
4. Calculate HMAC-SHA256 Signature

Step 1: Build Canonical Request ​

The canonical request is a standardized representation of your HTTP request.

Format ​

<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.

Components ​

HTTP Method ​

HTTP method in uppercase.

POST
GET
PUT
DELETE
PATCH

Request Path ​

The path component of the URL (without query string).

/api/data
/api/devices/123
/api/users

Query String ​

URL-encoded query parameters (without leading ?). If no query parameters, use empty string.

typescript
// 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 string

Important: Query parameters should keep a stable order between signer and verifier.

typescript
// ❌ Wrong
const queryString = 'limit=10&status=active';

// ✅ Correct (stable order)
const queryString = 'limit=10&status=active';  // 'limit' comes before 'status'

Canonical Headers ​

Signed headers in canonical format:

  • Header names in lowercase
  • Format: name:value (no spaces around colon)
  • Follow the exact server order
  • Separated by \n (newline)
  • Must end with newline (\n)

Example:

x-client-type:service
x-date:2025-10-14T03:52:12Z
x-request-id:bf6c5652-9656-4167-b1d4-10c690a72102

Rules:

typescript
// ✅ 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 newline

Signed Headers List ​

Semicolon-separated list of signed header names (lowercase, exact server order).

For Service Authentication:

x-date;x-request-id;x-client-type

For User Authentication:

x-date;x-tenant-id;x-request-id;x-client-type

Body Hash ​

SHA256 hash of the request body (lowercase hex).

For requests with body:

typescript
const body = '{"message": "Hello"}';
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
// 5d41402abc4b2a76b9719d911017c592

For requests without body (GET, DELETE):

typescript
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:

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Use this constant for GET/DELETE requests.

Complete Example ​

Request:

http
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
5d41402abc4b2a76b9719d911017c592

Implementation ​

TypeScript:

typescript
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:

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
    ])

Step 2: Hash Canonical Request ​

Hash the canonical request using SHA256.

typescript
const canonicalRequestHash = crypto
  .createHash('sha256')
  .update(canonicalRequest)
  .digest('hex');

Example:

typescript
// 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: 7f3a4e8c9d2b1a5e6f8c7d9a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c

Step 3: Create String to Sign ​

Combine algorithm, timestamp, and canonical request hash.

Format ​

HMAC-SHA256
<TIMESTAMP>
<CANONICAL_REQUEST_HASH>

Note: Each component is separated by a newline (\n).

Components ​

  1. Algorithm identifier: HMAC-SHA256
  2. Timestamp: Same as X-Date header
  3. Canonical request hash: From Step 2

Example ​

HMAC-SHA256
2025-10-14T03:52:12Z
7f3a4e8c9d2b1a5e6f8c7d9a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c

Implementation ​

TypeScript:

typescript
function createStringToSign(
  timestamp: string,
  canonicalRequest: string
): string {
  const canonicalHash = crypto
    .createHash('sha256')
    .update(canonicalRequest)
    .digest('hex');
  
  return [
    'HMAC-SHA256',
    timestamp,
    canonicalHash
  ].join('\n');
}

Python:

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
    ])

Step 4: Calculate HMAC Signature ​

Sign the string to sign using HMAC-SHA256 with your secret key.

Formula ​

signature = HMAC-SHA256(secretKey, stringToSign)

Implementation ​

TypeScript:

typescript
function calculateSignature(
  secretKey: string,
  stringToSign: string
): string {
  return crypto
    .createHmac('sha256', secretKey)
    .update(stringToSign)
    .digest('hex');
}

Python:

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:

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))
}

Complete Implementation Example ​

Service Authentication ​

TypeScript:

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=...

User Authentication ​

typescript
// 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=...

Validation Test Cases ​

Use these test cases to validate your implementation:

Test Case 1: POST with Body (Service Auth) ​

Input:

typescript
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:

4bf1c7b7f0e73b19e1d3f0d13a4e81d8ef9f98dbedbb8df53c09c59d9f51e0b4

Test Case 2: GET without Body (User Auth) ​

Input:

typescript
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:

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Test Case 3: PUT with Query Parameters ​

Input:

typescript
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

Common Mistakes ​

1. Wrong Header Case in Canonical Request ​

typescript
// ❌ Wrong
'X-Date:2025-10-14T03:52:12Z'

// ✅ Correct
'x-date:2025-10-14T03:52:12Z'

2. Headers Not Sorted Alphabetically ​

typescript
// ❌ Wrong
'x-date;x-request-id;x-client-type'

// ✅ Correct
'x-date;x-request-id;x-client-type'

3. Missing Query String in Canonical Request ​

typescript
// ❌ 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...

4. Using null Instead of Empty String ​

typescript
// ❌ 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');

5. Wrong Timestamp Format ​

typescript
// ❌ Wrong
'2025-10-14 03:52:12'
'2025-10-14T10:52:12+07:00'

// ✅ Correct
'2025-10-14T03:52:12.000Z'

6. Missing Trailing Newline in Canonical Headers ​

typescript
// ❌ 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';

7. Spaces Around Colon in Headers ​

typescript
// ❌ Wrong
'x-date: 2025-10-14T03:52:12Z'
'x-date :2025-10-14T03:52:12Z'

// ✅ Correct
'x-date:2025-10-14T03:52:12Z'

8. Wrong Authorization Format ​

typescript
// ❌ Wrong (old format)
Authorization: HMAC-SHA256 Credential=service:external-system-client, ...

// ✅ Correct (new format)
Authorization: HMAC-SHA256 CredentialType=service, CredentialId=external-system-client, ...

Debugging Signature Mismatches ​

If your signature doesn't match, debug step by step:

Step-by-Step Debug Process ​

typescript
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;
}

Quick Checklist ​

When debugging signature mismatches:

  • [ ] Body hash matches (use empty string for GET/DELETE)
  • [ ] Headers are lowercase in canonical request
  • [ ] Signed headers follow server order exactly
  • [ ] Canonical headers end with newline (\n)
  • [ ] No spaces around colons in headers
  • [ ] Query string included as separate component in canonical request
  • [ ] No trailing newline after body hash
  • [ ] Timestamp is in ISO 8601 UTC format
  • [ ] Secret key is correct and same on both sides
  • [ ] SignedHeaders list matches actual headers
  • [ ] Using new format: CredentialType and CredentialId (not Credential=type:id)

Performance Considerations ​

Caching ​

For repeated requests to the same endpoint, you can optimize by caching static parts:

typescript
// 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 };
}

Avoid Unnecessary Hashing ​

typescript
// ❌ 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;
}

Security Best Practices ​

  1. Never log secret keys - Even in debug mode
  2. Use constant-time comparison - When verifying signatures on server
  3. Validate timestamp - Before signature verification to fail fast
  4. Rotate keys regularly - Implement key rotation strategy
  5. Use secure random for Request-Id - Don't use predictable values
typescript
// ✅ 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;
}

Next Steps ​