QAS TechDocs

    HMAC Signature Guide

    Detailed guide for generating and troubleshooting HMAC signatures

    This guide provides detailed information about generating, validating, and troubleshooting HMAC signatures for the API.

    What is HMAC?

    HMAC (Hash-based Message Authentication Code) is a cryptographic signature that:

    • Verifies the request came from you
    • Ensures the request wasn't modified in transit
    • Provides an additional security layer beyond the Bearer token

    HMAC Algorithm

    The API uses HMAC-SHA256:

    • Hash Algorithm: SHA-256
    • Output: 64-character hexadecimal string
    • Secret: Your API secret key

    Message Construction

    Format

    {HTTP_METHOD}{REQUEST_PATH}{JSON_BODY}

    Rules

    1. No separators: Concatenate directly (no spaces or newlines)
    2. HTTP Method: Always uppercase (GET, POST, PUT, DELETE)
    3. Request Path: Full path starting with /v3/
    4. Query Parameters: Include in path if present
    5. JSON Body: Compact format (no spaces) for POST/PUT only

    Examples

    GET Request (No Body)

    Request: GET /v3/account/5001/balance
    
    Message: GET/v3/account/5001/balance

    GET with Query Parameters

    Request: GET /v3/account/list-my-pix-keys?account_branch_identifier=0001&account_number=123456
    
    Message: GET/v3/account/list-my-pix-keys?account_branch_identifier=0001&account_number=123456

    POST Request with Body

    Request: POST /v3/cashout/create-cashout
    Body: {"amount":100.50,"key":"user@example.com"}
    
    Message: POST/v3/cashout/create-cashout{"amount":100.50,"key":"user@example.com"}

    Important: Body must be compact JSON (no spaces between keys/values)

    Step-by-Step Generation

    Step 1: Construct the Message

    method = "POST"
    path = "/v3/cashout/create-cashout"
    body = {
        "amount": 100.50,
        "key": "user@example.com",
        "source_account_branch_identifier": "0001",
        "source_account_number": "123456"
    }
    
    # Build message
    message = f"{method}{path}"
    
    if body:
        import json
        # CRITICAL: Use separators=(',', ':') for compact JSON
        message += json.dumps(body, separators=(',', ':'))
    
    print(f"Message: {message}")
    # Output: POST/v3/cashout/create-cashout{"amount":100.5,"key":"user@example.com","source_account_branch_identifier":"0001","source_account_number":"123456"}

    Step 2: Generate HMAC

    import hmac
    import hashlib
    
    secret_key = "your_secret_key_here"
    
    # Generate HMAC-SHA256
    signature = hmac.new(
        secret_key.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    print(f"HMAC: {signature}")
    # Output: 64-character hex string

    Step 3: Add to Request Header

    Authorization: Bearer your_access_token
    hmac: generated_signature_here
    Content-Type: application/json

    Common Pitfalls

    1. Spaces in JSON

    ❌ Wrong:

    json.dumps(body)  # Default includes spaces
    # Output: {"amount": 100.5, "key": "user@example.com"}

    ✅ Correct:

    json.dumps(body, separators=(',', ':'))  # No spaces
    # Output: {"amount":100.5,"key":"user@example.com"}

    2. Including Domain in Path

    ❌ Wrong:

    path = "https://api.qastech.com.br/v3/account/5001/balance"

    ✅ Correct:

    path = "/v3/account/5001/balance"

    3. Lowercase HTTP Method

    ❌ Wrong:

    method = "get"
    message = f"{method}{path}"  # "get/v3/..."

    ✅ Correct:

    method = "GET"  # or method.upper()
    message = f"{method.upper()}{path}"  # "GET/v3/..."

    4. Wrong Character Encoding

    ❌ Wrong:

    signature = hmac.new(
        secret_key,  # Not encoded
        message,     # Not encoded
        hashlib.sha256
    ).hexdigest()

    ✅ Correct:

    signature = hmac.new(
        secret_key.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    5. Including Body in GET Requests

    ❌ Wrong:

    # GET request
    message = f"GET/v3/account/5001/balance{json.dumps(body)}"

    ✅ Correct:

    # GET request - no body
    message = f"GET/v3/account/5001/balance"

    Debugging HMAC Issues

    Enable Debug Logging

    def generate_hmac_debug(secret_key, method, path, body=None):
        """Generate HMAC with debug logging."""
        
        # Step 1: Construct message
        message = f"{method.upper()}{path}"
        print(f"1. Base message: {message}")
        
        # Step 2: Add body if present
        if body:
            body_json = json.dumps(body, separators=(',', ':'))
            print(f"2. Body JSON: {body_json}")
            message += body_json
            print(f"3. Complete message: {message}")
        
        # Step 3: Encode
        message_bytes = message.encode('utf-8')
        secret_bytes = secret_key.encode('utf-8')
        print(f"4. Message length: {len(message_bytes)} bytes")
        print(f"5. Secret length: {len(secret_bytes)} bytes")
        
        # Step 4: Generate HMAC
        signature = hmac.new(
            secret_bytes,
            message_bytes,
            hashlib.sha256
        ).hexdigest()
        
        print(f"6. HMAC signature: {signature}")
        print(f"7. Signature length: {len(signature)} characters")
        
        return signature

    Verify HMAC Locally

    def verify_hmac(secret_key, message, expected_signature):
        """Verify HMAC signature matches expected value."""
        
        actual_signature = hmac.new(
            secret_key.encode('utf-8'),
            message.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
        
        if actual_signature == expected_signature:
            print("✓ HMAC signatures match!")
            return True
        else:
            print("✗ HMAC signatures DO NOT match")
            print(f"Expected: {expected_signature}")
            print(f"Actual:   {actual_signature}")
            return False

    Test with Known Values

    def test_known_hmac():
        """Test with known good values."""
        
        # Known test case
        secret_key = "test_secret"
        method = "GET"
        path = "/v3/account/123/balance"
        
        # Expected message
        expected_message = "GET/v3/account/123/balance"
        
        # Generate HMAC
        signature = hmac.new(
            secret_key.encode('utf-8'),
            expected_message.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
        
        print(f"Test Message: {expected_message}")
        print(f"Test Signature: {signature}")
        
        # Verify signature format
        assert len(signature) == 64, "Signature must be 64 characters"
        assert signature.islower(), "Signature must be lowercase"
        assert all(c in '0123456789abcdef' for c in signature), "Signature must be hex"
        
        print("✓ All HMAC format checks passed")
    
    test_known_hmac()

    Language-Specific Considerations

    Python

    Unicode Handling:

    # Ensure UTF-8 encoding
    message.encode('utf-8')  # Explicit encoding

    JSON Formatting:

    # Use separators for compact JSON
    json.dumps(body, separators=(',', ':'))

    JavaScript

    String Encoding:

    // Node.js automatically handles UTF-8
    crypto.createHmac('sha256', secretKey).update(message, 'utf8')

    JSON Formatting:

    // JSON.stringify is compact by default (no spaces)
    JSON.stringify(body)

    PHP

    String Handling:

    // UTF-8 is default in PHP
    hash_hmac('sha256', $message, $secretKey)

    JSON Formatting:

    // Use JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
    json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)

    Java

    Charset:

    // Explicitly use UTF-8
    message.getBytes(StandardCharsets.UTF_8)

    JSON Library:

    // Use Gson for consistent JSON
    new Gson().toJson(body)

    Advanced Scenarios

    Query Parameters

    # Include query parameters in path
    path = "/v3/account/list-my-pix-keys"
    params = {
        "account_branch_identifier": "0001",
        "account_number": "123456"
    }
    
    # Build query string
    query_string = "&".join([f"{k}={v}" for k, v in params.items()])
    full_path = f"{path}?{query_string}"
    
    message = f"GET{full_path}"
    # Result: GET/v3/account/list-my-pix-keys?account_branch_identifier=0001&account_number=123456

    URL Encoding

    from urllib.parse import urlencode, quote
    
    # URL encode query parameters if needed
    params = {
        "tag": "invoice #123",  # Contains special character
    }
    
    # Option 1: Use urlencode
    encoded_params = urlencode(params)
    # Result: tag=invoice+%23123
    
    # Option 2: Manual encoding
    encoded_value = quote(params["tag"])
    # Result: invoice%20%23123
    
    # Use in path
    path = f"/v3/transactions?{encoded_params}"

    Important: Include encoded version in HMAC message if that's what you're sending in the actual request.

    Large Payloads

    # For large JSON bodies, ensure consistent formatting
    import json
    
    body = {
        "field1": "value1",
        "field2": "value2",
        # ... many more fields
    }
    
    # Use sort_keys for consistent order
    body_json = json.dumps(body, separators=(',', ':'), sort_keys=True)

    Special Characters

    # Handle special characters in JSON values
    body = {
        "description": "Payment for invoice #123 & delivery",
        "tag": "invoice/2024/05"
    }
    
    # JSON encoding handles escaping automatically
    body_json = json.dumps(body, separators=(',', ':'))
    # Result: {"description":"Payment for invoice #123 & delivery","tag":"invoice/2024/05"}

    Security Considerations

    1. Constant-Time Comparison

    import hmac
    
    def verify_webhook_hmac(received_signature, calculated_signature):
        """Verify HMAC using constant-time comparison."""
        # Prevents timing attacks
        return hmac.compare_digest(received_signature, calculated_signature)

    2. Secret Key Storage

    # ✅ Good: Environment variables
    import os
    SECRET_KEY = os.getenv('API_SECRET_KEY')
    
    # ✅ Better: Secrets manager
    import boto3
    def get_secret():
        client = boto3.client('secretsmanager')
        response = client.get_secret_value(SecretId='api-secret')
        return response['SecretString']

    3. Key Rotation

    class APIClient:
        def __init__(self, secret_key, secret_key_new=None):
            self.secret_key = secret_key
            self.secret_key_new = secret_key_new  # For rotation period
        
        def generate_hmac(self, method, path, body=None):
            # Try new key first during rotation
            if self.secret_key_new:
                return self._generate(self.secret_key_new, method, path, body)
            return self._generate(self.secret_key, method, path, body)

    Performance Optimization

    Cache HMAC for Retries

    class APIRequest:
        def __init__(self, method, path, body=None):
            self.method = method
            self.path = path
            self.body = body
            self._hmac_cache = None
        
        def get_hmac(self, secret_key):
            """Generate or return cached HMAC."""
            if self._hmac_cache is None:
                self._hmac_cache = generate_hmac(
                    secret_key, self.method, self.path, self.body
                )
            return self._hmac_cache

    Reuse HMAC Context

    import hmac
    import hashlib
    
    # For multiple signatures with same key
    hmac_context = hmac.new(secret_key.encode(), digestmod=hashlib.sha256)
    
    # Clone for each message
    sig1 = hmac_context.copy()
    sig1.update(message1.encode())
    signature1 = sig1.hexdigest()
    
    sig2 = hmac_context.copy()
    sig2.update(message2.encode())
    signature2 = sig2.hexdigest()

    Validation Checklist

    Before making a request, verify:

    • HTTP method is uppercase
    • Path starts with /v3/
    • Path includes query parameters (if any)
    • Body is compact JSON (no spaces) for POST/PUT
    • Body is omitted for GET/DELETE
    • Message uses UTF-8 encoding
    • Secret key uses UTF-8 encoding
    • HMAC is 64 characters long
    • HMAC is lowercase hexadecimal
    • HMAC is sent in hmac header (not HMAC or x-hmac)

    Testing Tools

    Online HMAC Generator

    Visit: https://www.freeformatter.com/hmac-generator.html

    Settings:

    • Algorithm: SHA-256
    • Input type: Text String
    • Output encoding: Hex (lowercase)

    Command Line (OpenSSL)

    # Generate HMAC from command line
    echo -n "GET/v3/account/5001/balance" | \
      openssl dgst -sha256 -hmac "your_secret_key"

    Python Test Script

    Save as test_hmac.py:

    #!/usr/bin/env python3
    import hmac
    import hashlib
    import json
    import sys
    
    def main():
        if len(sys.argv) < 4:
            print("Usage: test_hmac.py <secret_key> <method> <path> [body_json]")
            sys.exit(1)
        
        secret_key = sys.argv[1]
        method = sys.argv[2].upper()
        path = sys.argv[3]
        body = json.loads(sys.argv[4]) if len(sys.argv) > 4 else None
        
        # Generate HMAC
        message = f"{method}{path}"
        if body:
            message += json.dumps(body, separators=(',', ':'))
        
        signature = hmac.new(
            secret_key.encode('utf-8'),
            message.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
        
        print(f"Message: {message}")
        print(f"HMAC: {signature}")
    
    if __name__ == '__main__':
        main()

    Usage:

    python test_hmac.py "secret" "GET" "/v3/account/123/balance"
    python test_hmac.py "secret" "POST" "/v3/cashout" '{"amount":100}'

    Next Steps


    Still having issues? When contacting support, include:

    1. Request method and path
    2. Generated message (without sensitive data)
    3. Generated HMAC signature
    4. Error response from API