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