DevToolBox무료
블로그

HMAC 생성기: 온라인으로 HMAC 서명 생성 — 완전 가이드

11분 읽기by DevToolBox

TL;DR

HMAC (Hash-based Message Authentication Code) combines a secret key with a hash function to verify both the integrity and authenticity of a message. Use HMAC-SHA256 for API request signing, webhook verification (GitHub, Stripe), and JWT HS256 signatures. Never use a plain hash like SHA-256(secret + message) — it is vulnerable to length extension attacks. Always use constant-time comparison to prevent timing attacks. Try our free online HMAC generator, or follow the code examples below for Node.js, Browser Web Crypto API, Python, and Go.

What Is HMAC? Hash-based Message Authentication Code Explained

HMAC (Hash-based Message Authentication Code) is a cryptographic construction defined in RFC 2104 that combines a secret key with a hash function to produce a message authentication code. While a plain hash like SHA-256 verifies only integrity (detecting accidental or deliberate data corruption), HMAC verifies both integrity and authenticity — proving that the message was created by someone who possesses the secret key.

The formal definition of HMAC is:

HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))

Where:
  K  = secret key (padded or hashed to block size as K')
  m  = the message to authenticate
  H  = hash function (SHA-256, SHA-512, etc.)
  ⊕  = XOR operation
  || = concatenation
  ipad = 0x36 repeated to block size (inner padding)
  opad = 0x5C repeated to block size (outer padding)

The double-hashing with different paddings prevents:
  - Length extension attacks (common with naive key || message constructions)
  - Related-key attacks
  - Weak key attacks

The two rounds of hashing with distinct key pads (ipad for the inner hash, opad for the outer hash) are what make HMAC provably secure even when the underlying hash function has certain weaknesses. This is why HMAC-SHA256 remained secure even as researchers found vulnerabilities in SHA-2 variants — the construction itself adds security beyond the underlying hash.

The Two Core Properties HMAC Provides

  • Message integrity: Any modification to the message — even a single bit — produces a completely different HMAC output. The receiver can detect tampering by recomputing the HMAC and comparing.
  • Message authenticity: Only a party that knows the secret key can produce the correct HMAC for a given message. This is fundamentally different from a plain hash — anyone can compute SHA-256("hello"), but only the key-holder can compute HMAC-SHA256(secret_key, "hello") correctly.

Note what HMAC does not provide: it does not provide non-repudiation (since both sender and receiver share the same key, either could have created the HMAC) and it does not provide confidentiality (the message is not encrypted). For non-repudiation, use digital signatures (RSA, ECDSA, Ed25519). For confidentiality, use authenticated encryption (AES-GCM, ChaCha20-Poly1305).

HMAC vs Hash vs Digital Signature — Comparison Table

Understanding when to use each cryptographic primitive is essential. Here is a comprehensive comparison:

PropertyPlain Hash (SHA-256)HMAC-SHA256Digital Signature (RSA/ECDSA)
Key required?No (anyone can compute)Yes (symmetric secret key)Yes (private key to sign, public key to verify)
Verifies integrityYesYesYes
Verifies authenticityNoYes (symmetric)Yes (asymmetric)
Non-repudiationNoNo (both parties share key)Yes (only private key owner can sign)
Key distributionN/ARequires secure out-of-band key sharingPublic key can be shared openly
PerformanceVery fastVery fast (~2x hash)Slower (RSA: ~1000x slower than hash)
Common use casesFile checksums, content addressingAPI signing, webhooks, JWT HS256, session tokensTLS certificates, code signing, JWT RS256
Length extension attackVulnerable (SHA-2)Not vulnerableNot vulnerable

HMAC Algorithm Variants — SHA256, SHA512, SHA1, MD5

HMAC is a construction, not a specific algorithm — it can be used with any hash function. The security of HMAC depends on both the hash function and the key length:

AlgorithmOutput SizeSecurity LevelStatusUse Case
HMAC-SHA25632 bytes (256 bits)128-bitRecommendedWebhooks, API signing, JWT HS256, AWS SigV4
HMAC-SHA51264 bytes (512 bits)256-bitRecommended (high security)High-security APIs, JWT HS512, long-term keys
HMAC-SHA38448 bytes (384 bits)192-bitAcceptableJWT HS384, intermediate security needs
HMAC-SHA120 bytes (160 bits)80-bitDeprecated (legacy only)TOTP (RFC 4226), legacy OAuth 1.0 — avoid for new systems
HMAC-MD516 bytes (128 bits)64-bitDo not useLegacy systems only — never for new code

A key insight: HMAC-SHA1 and HMAC-MD5 are not broken in the same way as raw SHA-1 and MD5. The HMAC construction prevents collision attacks from being directly applicable. However, their small output sizes (80-bit and 64-bit security respectively) make them insufficiently secure against modern brute-force attacks. Stick with HMAC-SHA256 or HMAC-SHA512.

JavaScript — Node.js crypto Module

Node.js provides built-in HMAC support via the crypto module. The createHmac function returns an HMAC object that supports streaming (chunked) input via .update().

import crypto from 'crypto';

// --- Basic HMAC-SHA256 ---
function hmacSha256(secret: string, message: string): string {
  return crypto.createHmac('sha256', secret)
    .update(message, 'utf8')
    .digest('hex');
}

const signature = hmacSha256('my-secret-key-32-bytes-minimum!!', 'payload data');
console.log(signature);
// => "a6b4f7c9d2e1..." (64 hex chars = 32 bytes)

// --- HMAC-SHA256 with Base64 encoding ---
function hmacSha256Base64(secret: string, message: string): string {
  return crypto.createHmac('sha256', secret)
    .update(message)
    .digest('base64');
}

// --- HMAC-SHA512 ---
function hmacSha512(secret: string, message: string): string {
  return crypto.createHmac('sha512', secret)
    .update(message, 'utf8')
    .digest('hex');
}

// --- Streaming HMAC (for large payloads) ---
import { createReadStream } from 'fs';

async function hmacFile(secret: string, filePath: string): Promise<string> {
  return new Promise((resolve, reject) => {
    const hmac = crypto.createHmac('sha256', secret);
    const stream = createReadStream(filePath);
    stream.on('data', chunk => hmac.update(chunk));
    stream.on('end', () => resolve(hmac.digest('hex')));
    stream.on('error', reject);
  });
}

// --- CRITICAL: Constant-time comparison to prevent timing attacks ---
function verifyHmac(
  secret: string,
  message: string,
  receivedSignature: string
): boolean {
  const expected = hmacSha256(secret, message);
  // Both buffers must have the same length for timingSafeEqual
  // If lengths differ, they can't be equal anyway — but we still
  // must not leak timing information about which byte failed
  const expectedBuf = Buffer.from(expected, 'hex');
  let receivedBuf: Buffer;
  try {
    receivedBuf = Buffer.from(receivedSignature, 'hex');
  } catch {
    return false;
  }
  if (expectedBuf.length !== receivedBuf.length) {
    return false; // lengths differ — definitely not equal
  }
  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
  // NEVER use: expected === receivedSignature (vulnerable to timing attack)
}

// --- Generate a cryptographically random HMAC key ---
function generateHmacKey(bytes = 32): string {
  return crypto.randomBytes(bytes).toString('hex');
  // 32 bytes = 64 hex chars = 256 bits of entropy
}

const key = generateHmacKey(32);
console.log(`Generated key: ${key}`);

Node.js — HMAC with Binary Keys

import crypto from 'crypto';

// Keys can be strings, Buffers, or KeyObjects
// Using a Buffer key (raw bytes, not hex string)
const rawKey = crypto.randomBytes(32); // 32-byte Buffer
const hmac = crypto.createHmac('sha256', rawKey);
hmac.update('message to authenticate');
const sig = hmac.digest('hex');

// KeyObject API (Node.js 15+) — more explicit key handling
const key = crypto.createSecretKey(rawKey);
const hmac2 = crypto.createHmac('sha256', key);
hmac2.update('message');
const sig2 = hmac2.digest('hex');

// Multiple update() calls — useful for structured data
function hmacStructuredPayload(
  secret: string,
  method: string,
  path: string,
  timestamp: string,
  body: string
): string {
  const hmac = crypto.createHmac('sha256', secret);
  // Update with each field separately — equivalent to concatenated string
  hmac.update(method);
  hmac.update('\n');
  hmac.update(path);
  hmac.update('\n');
  hmac.update(timestamp);
  hmac.update('\n');
  hmac.update(body);
  return hmac.digest('hex');
}

JavaScript — Browser Web Crypto API (SubtleCrypto)

The Web Crypto API is available natively in all modern browsers (Chrome 37+, Firefox 34+, Safari 11+, Edge 12+) and in Node.js 18+ as globalThis.crypto.subtle. It uses SubtleCrypto.sign() for HMAC generation and SubtleCrypto.verify() for verification. All operations return Promises and use ArrayBuffer for binary data.

// Web Crypto API — works in all modern browsers and Node.js 18+
// No dependencies required!

// Helper: ArrayBuffer to hex string
function bufferToHex(buffer: ArrayBuffer): string {
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

// Helper: ArrayBuffer to base64 string
function bufferToBase64(buffer: ArrayBuffer): string {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

// --- HMAC-SHA256 sign ---
async function hmacSha256Sign(
  secret: string,
  message: string
): Promise<string> {
  const encoder = new TextEncoder();

  // Step 1: Import the key
  const key = await crypto.subtle.importKey(
    'raw',                           // key format
    encoder.encode(secret),          // key material as ArrayBuffer
    { name: 'HMAC', hash: 'SHA-256' }, // algorithm descriptor
    false,                           // not extractable
    ['sign']                         // key usage
  );

  // Step 2: Sign the message
  const signature = await crypto.subtle.sign(
    'HMAC',
    key,
    encoder.encode(message)
  );

  return bufferToHex(signature);
}

// Usage
const sig = await hmacSha256Sign('my-secret-key', 'hello world');
console.log(sig); // => "3b5f..." (64 hex chars)

// --- HMAC-SHA256 verify (built-in constant-time comparison!) ---
async function hmacSha256Verify(
  secret: string,
  message: string,
  signatureHex: string
): Promise<boolean> {
  const encoder = new TextEncoder();

  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify']                       // key usage: verify
  );

  // Convert hex signature back to ArrayBuffer
  const sigBytes = new Uint8Array(
    signatureHex.match(/.{2}/g)!.map(b => parseInt(b, 16))
  );

  // SubtleCrypto.verify() uses constant-time comparison internally!
  return crypto.subtle.verify(
    'HMAC',
    key,
    sigBytes,
    encoder.encode(message)
  );
}

// --- HMAC-SHA512 (just change the hash parameter) ---
async function hmacSha512Sign(
  secret: string,
  message: string
): Promise<string> {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-512' }, // <-- SHA-512 here
    false,
    ['sign']
  );
  const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
  return bufferToHex(sig);
}

// --- Reuse imported keys for performance ---
// Importing a key is relatively expensive — cache it when signing many messages
async function createHmacSigner(secret: string) {
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign', 'verify']
  );

  return {
    sign: async (message: string): Promise<string> => {
      const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message));
      return bufferToHex(sig);
    },
    verify: async (message: string, sigHex: string): Promise<boolean> => {
      const sigBytes = new Uint8Array(sigHex.match(/.{2}/g)!.map(b => parseInt(b, 16)));
      return crypto.subtle.verify('HMAC', key, sigBytes, new TextEncoder().encode(message));
    }
  };
}

const signer = await createHmacSigner('my-secret-key');
const sig1 = await signer.sign('message 1');
const sig2 = await signer.sign('message 2');
const valid = await signer.verify('message 1', sig1); // => true

Python — hmac Module

Python's built-in hmac module provides HMAC generation and constant-time comparison via hmac.compare_digest(). Unlike Node.js's streaming API, Python's HMAC object also supports chunked updates via the .update() method, compatible with the hashlib API.

import hmac
import hashlib
import os
import secrets

# --- Basic HMAC-SHA256 ---
def hmac_sha256(secret: bytes, message: bytes) -> str:
    """Compute HMAC-SHA256 and return hex digest."""
    h = hmac.new(secret, message, hashlib.sha256)
    return h.hexdigest()

# Usage — note: both secret and message must be bytes
secret = b"my-secret-key-32-bytes-minimum!!"
message = b"payload data"

signature = hmac_sha256(secret, message)
print(signature)  # => "a6b4..." (64 hex chars)

# --- HMAC-SHA512 ---
def hmac_sha512(secret: bytes, message: bytes) -> str:
    h = hmac.new(secret, message, hashlib.sha512)
    return h.hexdigest()

# --- HMAC with string inputs (encode to bytes) ---
def hmac_sha256_str(secret: str, message: str) -> str:
    return hmac_sha256(secret.encode('utf-8'), message.encode('utf-8'))

# --- Base64 output ---
import base64

def hmac_sha256_base64(secret: bytes, message: bytes) -> str:
    h = hmac.new(secret, message, hashlib.sha256)
    return base64.b64encode(h.digest()).decode()

# --- Streaming / chunked update ---
def hmac_sha256_chunked(secret: bytes, chunks: list[bytes]) -> str:
    h = hmac.new(secret, digestmod=hashlib.sha256)
    for chunk in chunks:
        h.update(chunk)
    return h.hexdigest()

# For large files:
def hmac_file(secret: bytes, filepath: str) -> str:
    h = hmac.new(secret, digestmod=hashlib.sha256)
    with open(filepath, 'rb') as f:
        while chunk := f.read(8192):
            h.update(chunk)
    return h.hexdigest()

# --- CRITICAL: Constant-time comparison ---
def verify_hmac(secret: bytes, message: bytes, received: str) -> bool:
    """Verify HMAC using constant-time comparison."""
    expected = hmac_sha256(secret, message)
    # hmac.compare_digest prevents timing attacks
    # It compares byte-by-byte in constant time
    return hmac.compare_digest(expected, received)
    # NEVER use: expected == received  (timing attack vulnerable!)

# --- Generate a cryptographically random key ---
def generate_key(length: int = 32) -> bytes:
    """Generate a cryptographically secure random HMAC key."""
    return secrets.token_bytes(length)
    # OR: return os.urandom(length)

key = generate_key(32)
print(key.hex())  # => 64 hex chars (256-bit key)

# --- Practical example: API request signing ---
import time
import json

def sign_api_request(
    secret: bytes,
    method: str,
    path: str,
    body: str = ""
) -> dict[str, str]:
    timestamp = str(int(time.time()))
    payload = f"{method}\n{path}\n{timestamp}\n{body}"
    signature = hmac_sha256(secret, payload.encode('utf-8'))
    return {
        "X-Timestamp": timestamp,
        "X-Signature": signature,
    }

headers = sign_api_request(
    secret=b"api-signing-secret-key-32bytes!!",
    method="POST",
    path="/api/v1/orders",
    body=json.dumps({"qty": 1, "item": "widget"})
)
print(headers)

Go — crypto/hmac Package

Go's standard library provides the crypto/hmac package with hmac.New() for creation and hmac.Equal() for constant-time comparison. The package follows Go's io.Writer interface, allowing streaming updates.

package main

import (
    "crypto/hmac"
    "crypto/rand"
    "crypto/sha256"
    "crypto/sha512"
    "encoding/hex"
    "fmt"
    "io"
    "os"
    "time"
)

// --- Basic HMAC-SHA256 ---
func HmacSha256(secret, message []byte) string {
    mac := hmac.New(sha256.New, secret)
    mac.Write(message)
    return hex.EncodeToString(mac.Sum(nil))
}

// --- HMAC-SHA512 ---
func HmacSha512(secret, message []byte) string {
    mac := hmac.New(sha512.New, secret)
    mac.Write(message)
    return hex.EncodeToString(mac.Sum(nil))
}

// --- CRITICAL: Constant-time HMAC verification ---
func VerifyHmacSha256(secret, message []byte, receivedHex string) bool {
    expected := HmacSha256(secret, message)
    receivedBytes, err := hex.DecodeString(receivedHex)
    if err != nil {
        return false
    }
    expectedBytes, _ := hex.DecodeString(expected)
    // hmac.Equal uses constant-time comparison (subtle.ConstantTimeCompare)
    return hmac.Equal(expectedBytes, receivedBytes)
    // NEVER use: expected == receivedHex  (timing attack vulnerable!)
}

// --- Streaming HMAC for large data ---
func HmacSha256Stream(secret []byte, reader io.Reader) (string, error) {
    mac := hmac.New(sha256.New, secret)
    if _, err := io.Copy(mac, reader); err != nil {
        return "", fmt.Errorf("hmac stream error: %w", err)
    }
    return hex.EncodeToString(mac.Sum(nil)), nil
}

// --- File HMAC ---
func HmacFile(secret []byte, path string) (string, error) {
    f, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer f.Close()
    return HmacSha256Stream(secret, f)
}

// --- Generate cryptographically random key ---
func GenerateKey(size int) ([]byte, error) {
    key := make([]byte, size)
    if _, err := rand.Read(key); err != nil {
        return nil, fmt.Errorf("key generation failed: %w", err)
    }
    return key, nil
}

// --- API request signing pattern ---
func SignRequest(secret []byte, method, path, body string) (string, string) {
    timestamp := fmt.Sprintf("%d", time.Now().Unix())
    payload := fmt.Sprintf("%s\n%s\n%s\n%s", method, path, timestamp, body)
    mac := hmac.New(sha256.New, secret)
    mac.Write([]byte(payload))
    return timestamp, hex.EncodeToString(mac.Sum(nil))
}

func main() {
    // Generate a secure key
    key, err := GenerateKey(32)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Key: %s\n", hex.EncodeToString(key))

    // Sign a message
    sig := HmacSha256(key, []byte("hello, world"))
    fmt.Printf("HMAC-SHA256: %s\n", sig)

    // Verify
    valid := VerifyHmacSha256(key, []byte("hello, world"), sig)
    fmt.Printf("Valid: %v\n", valid) // => true

    // Tampered message
    invalid := VerifyHmacSha256(key, []byte("hello, world!"), sig)
    fmt.Printf("Tampered: %v\n", invalid) // => false

    // API signing
    ts, requestSig := SignRequest(key, "POST", "/api/orders", `{"qty":1}`)
    fmt.Printf("Timestamp: %s\nSignature: %s\n", ts, requestSig)
}

GitHub Webhook Verification — Complete Implementation

GitHub webhooks are one of the most common real-world applications of HMAC. When a repository event occurs (push, pull request, release, etc.), GitHub sends an HTTP POST request to your endpoint with the payload signed using HMAC-SHA256.

// GitHub Webhook Verification — Node.js/Express
import crypto from 'crypto';
import express from 'express';

const app = express();

// CRITICAL: Use express.raw() to get the raw body buffer
// express.json() or body-parser would parse and re-serialize JSON,
// changing whitespace and field ordering which breaks HMAC verification
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
  const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET!;

  // 1. Get the signature GitHub sent
  const githubSignature = req.headers['x-hub-signature-256'] as string;
  if (!githubSignature) {
    return res.status(401).json({ error: 'Missing X-Hub-Signature-256 header' });
  }

  // 2. Compute HMAC-SHA256 of the raw body
  const rawBody = req.body; // Buffer (because of express.raw())
  const computedHmac = crypto.createHmac('sha256', webhookSecret)
    .update(rawBody)
    .digest('hex');
  const computedSignature = `sha256=${computedHmac}`;

  // 3. Constant-time comparison — prevent timing attacks
  const sigBuf = Buffer.from(githubSignature, 'utf8');
  const computedBuf = Buffer.from(computedSignature, 'utf8');

  if (sigBuf.length !== computedBuf.length) {
    return res.status(401).json({ error: 'Signature length mismatch' });
  }

  if (!crypto.timingSafeEqual(sigBuf, computedBuf)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 4. Signature valid — process the event
  const event = req.headers['x-github-event'];
  const payload = JSON.parse(rawBody.toString('utf8'));

  console.log(`GitHub event: ${event}, repo: ${payload.repository?.full_name}`);

  res.status(200).json({ received: true });
});
# GitHub Webhook Verification — Python (Flask)
import hmac
import hashlib
import os
from flask import Flask, request, abort, jsonify

app = Flask(__name__)

def verify_github_webhook(payload_body: bytes, signature_header: str | None) -> bool:
    """Verify that the webhook payload was sent from GitHub."""
    if not signature_header:
        return False

    webhook_secret = os.environ.get('GITHUB_WEBHOOK_SECRET', '').encode('utf-8')

    # Compute expected HMAC-SHA256
    expected_sig = 'sha256=' + hmac.new(
        webhook_secret,
        payload_body,
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(expected_sig, signature_header)

@app.route('/webhook', methods=['POST'])
def github_webhook():
    # Use request.data (raw bytes), NOT request.json (parsed dict)
    payload_body = request.data
    signature = request.headers.get('X-Hub-Signature-256')

    if not verify_github_webhook(payload_body, signature):
        abort(401, 'Invalid webhook signature')

    event = request.headers.get('X-GitHub-Event')
    import json
    payload = json.loads(payload_body)

    print(f"GitHub event: {event}, repo: {payload.get('repository', {}).get('full_name')}")
    return jsonify({'received': True})

AWS Signature Version 4 — HMAC-SHA256 Chain

AWS Signature Version 4 (SigV4) is one of the most sophisticated real-world HMAC applications. It uses a chain of HMAC-SHA256 operations to derive a signing key that is scoped to a specific date, region, and service. Understanding this helps with debugging AWS SDK authentication issues.

import crypto from 'crypto';

// AWS Signature Version 4 — HMAC-SHA256 key derivation chain
// This is what the AWS SDK does internally for every request

function hmacSha256(key: Buffer | string, data: string): Buffer {
  return crypto.createHmac('sha256', key).update(data, 'utf8').digest();
}

function sha256Hex(data: string): string {
  return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
}

interface AwsCredentials {
  accessKeyId: string;
  secretAccessKey: string;
  region: string;
  service: string;
}

function deriveSigningKey(
  credentials: AwsCredentials,
  dateString: string  // format: YYYYMMDD
): Buffer {
  // 4-step HMAC-SHA256 key derivation
  const kDate    = hmacSha256('AWS4' + credentials.secretAccessKey, dateString);
  const kRegion  = hmacSha256(kDate, credentials.region);
  const kService = hmacSha256(kRegion, credentials.service);
  const kSigning = hmacSha256(kService, 'aws4_request');
  return kSigning;
}

function signRequest(
  credentials: AwsCredentials,
  method: string,
  canonicalUri: string,
  canonicalQueryString: string,
  headers: Record<string, string>,
  body: string
): string {
  const now = new Date();
  const dateString = now.toISOString().slice(0, 10).replace(/-/g, '');  // YYYYMMDD
  const datetimeString = now.toISOString().replace(/[:-]/g, '').slice(0, 15) + 'Z';  // YYYYMMDDTHHmmssZ

  // Step 1: Canonical request
  const signedHeaders = Object.keys(headers).sort().join(';');
  const canonicalHeaders = Object.entries(headers)
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([k, v]) => `${k.toLowerCase()}:${v.trim()}\n`)
    .join('');
  const payloadHash = sha256Hex(body);
  const canonicalRequest = [
    method, canonicalUri, canonicalQueryString,
    canonicalHeaders, signedHeaders, payloadHash
  ].join('\n');

  // Step 2: String to sign
  const credentialScope = `${dateString}/${credentials.region}/${credentials.service}/aws4_request`;
  const stringToSign = [
    'AWS4-HMAC-SHA256',
    datetimeString,
    credentialScope,
    sha256Hex(canonicalRequest)
  ].join('\n');

  // Step 3: Signing key (derived from the HMAC chain)
  const signingKey = deriveSigningKey(credentials, dateString);

  // Step 4: Final HMAC-SHA256 signature
  const signature = crypto.createHmac('sha256', signingKey)
    .update(stringToSign, 'utf8')
    .digest('hex');

  return `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
}

JWT HS256 — HMAC-SHA256 Token Signing

JWT (JSON Web Token) with the HS256 algorithm uses HMAC-SHA256 to sign the token. The “HS” prefix indicates HMAC-SHA256; HS384 uses HMAC-SHA384; HS512 uses HMAC-SHA512. Here is the complete implementation from scratch to understand exactly how it works:

import crypto from 'crypto';

// JWT HS256 from scratch — understanding the internals
// (In production, use a library like jose or jsonwebtoken)

function base64UrlEncode(data: string | Buffer): string {
  const b64 = Buffer.from(data).toString('base64');
  return b64.replace(/=/g, '').replace(/+/g, '-').replace(///g, '_');
}

function base64UrlDecode(b64url: string): Buffer {
  const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
  const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
  return Buffer.from(padded, 'base64');
}

interface JwtPayload {
  sub?: string;
  name?: string;
  iat?: number;
  exp?: number;
  [key: string]: unknown;
}

function createJwt(secret: string, payload: JwtPayload): string {
  const header = { alg: 'HS256', typ: 'JWT' };

  // Add issued-at time if not present
  if (!payload.iat) {
    payload = { ...payload, iat: Math.floor(Date.now() / 1000) };
  }

  const encodedHeader = base64UrlEncode(JSON.stringify(header));
  const encodedPayload = base64UrlEncode(JSON.stringify(payload));
  const signingInput = `${encodedHeader}.${encodedPayload}`;

  // HMAC-SHA256 of the signing input (header.payload)
  const signature = crypto.createHmac('sha256', secret)
    .update(signingInput, 'utf8')
    .digest();

  return `${signingInput}.${base64UrlEncode(signature)}`;
}

function verifyJwt(
  secret: string,
  token: string
): { valid: boolean; payload?: JwtPayload; error?: string } {
  const parts = token.split('.');
  if (parts.length !== 3) {
    return { valid: false, error: 'Invalid JWT format' };
  }

  const [encodedHeader, encodedPayload, encodedSignature] = parts;
  const signingInput = `${encodedHeader}.${encodedPayload}`;

  // Recompute expected HMAC
  const expectedSig = crypto.createHmac('sha256', secret)
    .update(signingInput, 'utf8')
    .digest();
  const receivedSig = base64UrlDecode(encodedSignature);

  // Constant-time comparison
  if (expectedSig.length !== receivedSig.length) {
    return { valid: false, error: 'Signature length mismatch' };
  }
  if (!crypto.timingSafeEqual(expectedSig, receivedSig)) {
    return { valid: false, error: 'Invalid signature' };
  }

  // Decode and validate payload
  const payload = JSON.parse(base64UrlDecode(encodedPayload).toString('utf8')) as JwtPayload;

  // Check expiration
  if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
    return { valid: false, error: 'Token expired' };
  }

  return { valid: true, payload };
}

// Usage
const secret = 'my-jwt-secret-key-32-bytes-min!!';
const token = createJwt(secret, {
  sub: 'user123',
  name: 'Alice',
  exp: Math.floor(Date.now() / 1000) + 3600  // 1 hour
});

console.log(`JWT: ${token}`);
// => "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIi4uLn0.abc123..."

const result = verifyJwt(secret, token);
console.log(result);
// => { valid: true, payload: { sub: 'user123', name: 'Alice', ... } }

HMAC in HTTP API Authentication Headers

Many APIs (Stripe, Twilio, Shopify, AWS, Bitbucket) use HMAC for request signing instead of sending API keys in headers. The canonical pattern includes the timestamp to prevent replay attacks:

import crypto from 'crypto';

// --- HMAC-based API Client (sending signed requests) ---
class HmacApiClient {
  constructor(
    private readonly apiKeyId: string,
    private readonly apiSecret: string,
    private readonly baseUrl: string
  ) {}

  private sign(method: string, path: string, body: string, timestamp: string): string {
    // Include method, path, timestamp, and body hash in the signed payload
    const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
    const payload = [method.toUpperCase(), path, timestamp, bodyHash].join('\n');
    return crypto.createHmac('sha256', this.apiSecret).update(payload).digest('hex');
  }

  async request(method: string, path: string, body?: object): Promise<Response> {
    const timestamp = Date.now().toString();
    const bodyString = body ? JSON.stringify(body) : '';
    const signature = this.sign(method, path, bodyString, timestamp);

    return fetch(this.baseUrl + path, {
      method,
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': this.apiKeyId,
        'X-Timestamp': timestamp,
        'X-Signature': signature,
        // Alternative: put all in Authorization header
        // 'Authorization': `HMAC-SHA256 keyId="${this.apiKeyId}",timestamp="${timestamp}",signature="${signature}"`
      },
      body: bodyString || undefined,
    });
  }
}

// --- HMAC-based API Server (verifying incoming requests) ---
function createHmacVerifyMiddleware(secretResolver: (keyId: string) => Promise<string>) {
  return async (req: Request, keyId: string): Promise<boolean> => {
    const timestamp = req.headers.get('X-Timestamp');
    const receivedSig = req.headers.get('X-Signature');

    if (!timestamp || !receivedSig) return false;

    // Replay attack prevention: reject requests older than 5 minutes
    const requestAge = Date.now() - parseInt(timestamp);
    if (requestAge > 5 * 60 * 1000 || requestAge < -60 * 1000) {
      return false; // Clock skew tolerance: ±1 minute
    }

    const secret = await secretResolver(keyId);
    const body = await req.text();
    const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
    const url = new URL(req.url);
    const payload = [req.method, url.pathname, timestamp, bodyHash].join('\n');

    const expectedSig = crypto.createHmac('sha256', secret).update(payload).digest('hex');
    const expectedBuf = Buffer.from(expectedSig);
    const receivedBuf = Buffer.from(receivedSig);

    if (expectedBuf.length !== receivedBuf.length) return false;
    return crypto.timingSafeEqual(expectedBuf, receivedBuf);
  };
}

Security Best Practices for HMAC

1. Key Management

  • Minimum key length: 32 cryptographically random bytes (256 bits) for HMAC-SHA256. RFC 2104 requires the key to be at least as long as the hash output size.
  • Use a CSPRNG: Generate keys with crypto.randomBytes(32) (Node.js), secrets.token_bytes(32) (Python), or crypto/rand (Go). Never use Math.random() or random.random().
  • Store in secrets manager: AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, or Azure Key Vault. Never hardcode secrets in source code or store in environment variables in production.
  • Key rotation: Implement a key rotation strategy. During rotation, accept signatures from both old and new keys for a transition period (e.g., 24 hours), then retire the old key.
  • Never use passwords directly: If you must derive a key from a password, use HKDF, PBKDF2 (600,000+ iterations with SHA-256), scrypt, or Argon2. Plain passwords have much less entropy than random bytes.

2. Always Use Constant-Time Comparison

// WRONG — vulnerable to timing attack
if (computedSig === receivedSig) { ... }         // JavaScript
if computed_sig == received_sig:                  # Python
if computedSig == receivedSig { ... }             // Go

// CORRECT — constant-time comparison
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))  // Node.js
hmac.compare_digest(a, b)                               # Python
hmac.Equal([]byte(a), []byte(b))                        // Go
crypto.subtle.verify('HMAC', key, sig, data)            // Browser

3. Prevent Replay Attacks with Timestamps

// Include a timestamp in the signed payload to prevent replay attacks
// An attacker who intercepts a valid signed request cannot replay it later

const timestamp = Date.now().toString(); // milliseconds since epoch
const payload = `${method}\n${path}\n${timestamp}\n${bodyHash}`;
const signature = hmacSha256(secret, payload);

// Server-side: reject if timestamp is too old or in the future
function isTimestampValid(timestamp: string, toleranceMs = 300_000): boolean {
  const ts = parseInt(timestamp);
  const now = Date.now();
  return Math.abs(now - ts) < toleranceMs; // within ±5 minutes
}

// For even stronger replay protection: track used nonces in a cache
// const usedNonces = new Set<string>(); // or Redis with TTL
// if (usedNonces.has(nonce)) return false;
// usedNonces.add(nonce);

HKDF — HMAC-based Key Derivation Function

HKDF (RFC 5869) uses HMAC to derive multiple purpose-specific keys from a single master secret. It is the recommended way to derive session keys, encryption keys, or authentication keys from a shared secret (e.g., from a Diffie-Hellman exchange or a master key).

import crypto from 'crypto';

// HKDF — Derive purpose-specific keys from a master secret
// hkdfSync is available in Node.js 15.0.0+

function deriveKey(
  masterSecret: Buffer,
  info: string,           // context/purpose string (e.g., "session-key", "api-signing")
  salt?: Buffer,          // optional random salt
  length = 32            // output key length in bytes
): Buffer {
  // Node.js built-in hkdf
  return crypto.hkdfSync('sha256', masterSecret, salt || Buffer.alloc(0), info, length);
}

// Example: derive multiple keys from one master secret
const masterKey = crypto.randomBytes(32);

const sessionKey     = deriveKey(masterKey, 'session-encryption', undefined, 32);
const signingKey     = deriveKey(masterKey, 'request-signing', undefined, 32);
const cookieKey      = deriveKey(masterKey, 'cookie-authentication', undefined, 32);

// These are all cryptographically independent — compromising one does not
// compromise the others, even though they share the same master secret

// Python — HKDF (requires cryptography library: pip install cryptography)
/*
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
import os

def derive_key(master_secret: bytes, info: bytes, length: int = 32) -> bytes:
    hkdf = HKDF(
        algorithm=hashes.SHA256(),
        length=length,
        salt=None,  # or os.urandom(32) for random salt
        info=info,
    )
    return hkdf.derive(master_secret)

master = os.urandom(32)
signing_key = derive_key(master, b"request-signing")
session_key = derive_key(master, b"session-encryption")
*/

Timing Attacks — Why String Comparison is Dangerous

A timing attack is a side-channel attack where an attacker measures the time taken by cryptographic operations to extract secret information. For HMAC verification, the danger is in how strings are compared.

Consider a naive implementation of string comparison:

// How a typical string comparison works internally:
// (This is conceptually similar to early-exit string comparison)

function naiveCompare(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false; // EXITS EARLY on first mismatch!
  }
  return true;
}

// If a secret HMAC is "abc123def456..." and an attacker tries "xxxxxxxx...":
// - Try starting with 'a': comparison exits after 1 char (fast)  => starts with 'a'
// - Try starting with 'x': comparison exits after 0 chars (fast) => doesn't start with 'x'
// In practice, differences are nanoseconds to microseconds, but measurable with
// enough samples over a network.

// Constant-time comparison — always iterates all bytes:
function constantTimeCompare(a: Buffer, b: Buffer): boolean {
  if (a.length !== b.length) return false;
  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a[i] ^ b[i]; // XOR: 0 if equal, non-zero if different
    // Bitwise OR accumulates differences — never short-circuits
  }
  return result === 0;
}

// Node.js provides this as crypto.timingSafeEqual(a, b)
// Use it for ALL HMAC verification!

Testing Your HMAC Implementation — Test Vectors

RFC 2104 and RFC 4231 provide official test vectors for HMAC. Always verify your implementation against these before deploying to production:

// RFC 4231 Test Vectors for HMAC-SHA256

const testVectors = [
  {
    // Test Case 1: Basic test
    key:     Buffer.from('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', 'hex'),
    message: Buffer.from('4869205468657265', 'hex'), // "Hi There"
    expected: 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7',
  },
  {
    // Test Case 2: Key shorter than hash block size
    key:     Buffer.from('4a656665', 'hex'), // "Jefe"
    message: Buffer.from('7768617420646f2079612077616e7420666f72206e6f7468696e673f', 'hex'),
    // "what do ya want for nothing?"
    expected: '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964a72424',
  },
  {
    // Test Case 3: Data and key = 20 bytes of 0xaa and 0xdd
    key:     Buffer.alloc(20, 0xaa),
    message: Buffer.alloc(50, 0xdd),
    expected: '773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe',
  },
];

import crypto from 'crypto';

for (const [i, vec] of testVectors.entries()) {
  const computed = crypto.createHmac('sha256', vec.key)
    .update(vec.message)
    .digest('hex');

  const pass = computed === vec.expected;
  console.log(`Test ${i + 1}: ${pass ? '✓ PASS' : '✗ FAIL'}`);
  if (!pass) {
    console.log(`  Expected: ${vec.expected}`);
    console.log(`  Got:      ${computed}`);
  }
}

// Quick sanity check — compare with known values
// HMAC-SHA256(key="key", data="The quick brown fox jumps over the lazy dog")
// = f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8

const quickCheck = crypto.createHmac('sha256', 'key')
  .update('The quick brown fox jumps over the lazy dog')
  .digest('hex');
console.log(quickCheck);
// => "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"

Key Takeaways

  • HMAC requires a secret key — unlike a plain hash, it proves authenticity (only someone with the key could have created it).
  • HMAC-SHA256 is the industry standard for webhook verification, API signing, and JWT HS256 — never use HMAC-MD5 or HMAC-SHA1 for new systems.
  • Never construct HMAC manually as SHA-256(key + message) — this naive construction is vulnerable to length extension attacks.
  • Always use constant-time comparison (timingSafeEqual, hmac.compare_digest, hmac.Equal) when verifying HMAC signatures to prevent timing attacks.
  • Secret keys should be at minimum 32 random bytes (256 bits) — never use passwords directly as HMAC keys without key derivation.
  • HMAC-SHA512 provides a larger security margin but longer output; use it for high-security contexts where 64-byte signatures are acceptable.
  • HKDF (HMAC-based Key Derivation Function) is the recommended way to derive purpose-specific keys from a master secret.
  • AWS Signature Version 4 uses a chain of HMAC-SHA256 computations to derive signing keys — understanding HMAC is essential for AWS SDK internals.

Frequently Asked Questions

What is HMAC and how is it different from a regular hash?

HMAC (Hash-based Message Authentication Code) is a keyed-hash construction defined in RFC 2104. Unlike a plain hash (e.g., SHA-256("data")), HMAC takes both a message and a secret key as input: HMAC(K, m) = H((K ⊕ opad) || H((K ⊕ ipad) || m)). This construction provides two properties that a plain hash cannot: (1) authenticity — only someone who knows the secret key can produce the correct HMAC, and (2) integrity — any modification to the message changes the HMAC output. Plain hashes only verify integrity if the hash itself is kept secret, which requires a separate channel. HMAC is used for webhook verification (GitHub, Stripe), API request signing, JWT HS256 signatures, and cookie/session token validation.

Why should I not use SHA-256(secret + message) instead of HMAC?

The naive construction SHA-256(secret || message) is vulnerable to a length extension attack against SHA-2 family hash functions (SHA-256, SHA-512). An attacker who knows SHA-256(secret || message) and the length of the secret can compute SHA-256(secret || message || attacker_data) without knowing the secret key. This is because SHA-2 uses a Merkle-Damgard construction where the internal state can be resumed. HMAC specifically prevents this by applying two rounds of hashing with different key padding (ipad and opad), making the internal state inaccessible. SHA-3 (sponge construction) and BLAKE3 are not vulnerable to length extension attacks, but HMAC-SHA256 remains the standard.

What is a timing attack and why do I need constant-time comparison?

A timing attack exploits the fact that string comparison with === (or strcmp) returns early as soon as it finds a mismatch. An attacker can measure tiny differences in response time (microseconds to milliseconds) to determine how many bytes of their guessed signature match the correct one, eventually recovering the full signature. This allows signature forgery without the secret key. Constant-time comparison functions (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python, hmac.Equal in Go) always compare every byte before returning, making the comparison time independent of the data. Always use these functions when comparing HMAC signatures in security-sensitive code.

What is the minimum recommended key length for HMAC?

RFC 2104 recommends that the HMAC key be at least as long as the hash output. For HMAC-SHA256, that is 32 bytes (256 bits). For HMAC-SHA512, that is 64 bytes (512 bits). In practice, use at least 32 cryptographically random bytes for any HMAC key. Never use human-memorable passwords directly as HMAC keys — passwords have far less entropy than random bytes. If you must derive a key from a password or passphrase, use a proper key derivation function: HKDF for key expansion, PBKDF2 (600,000+ iterations), bcrypt, scrypt, or Argon2 for password-based KDF. Keys should be rotated periodically and stored in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.).

What is the difference between HMAC-SHA256, HMAC-SHA512, HMAC-SHA1, and HMAC-MD5?

HMAC-SHA256 produces a 32-byte (256-bit) signature, provides 128-bit security level, and is the current industry standard. HMAC-SHA512 produces a 64-byte (512-bit) signature, provides 256-bit security level, and is faster than HMAC-SHA256 on 64-bit processors due to SHA-512 using 64-bit operations. HMAC-SHA1 produces a 20-byte signature — while HMAC-SHA1 itself is not broken (unlike raw SHA-1), it provides only 80-bit security and should not be used for new systems. HMAC-MD5 produces a 16-byte signature — again, HMAC construction protects against MD5 collision attacks, but HMAC-MD5 provides only 64-bit security and is deprecated. For new systems, always choose HMAC-SHA256 or HMAC-SHA512.

How does GitHub webhook verification work with HMAC?

When you configure a GitHub webhook, you provide a secret string. GitHub computes HMAC-SHA256 of the raw request body using your secret as the key, then sends the result in the X-Hub-Signature-256 header as "sha256=<hex-digest>". Your server must: (1) extract the X-Hub-Signature-256 header, (2) compute HMAC-SHA256 of the raw request body using your shared secret, (3) prepend "sha256=" to your computed digest, (4) compare the two strings using a constant-time comparison function. You must use the raw, unparsed request body — parsing JSON first changes whitespace and field ordering. The verification fails if the webhook secret is wrong, if you use the parsed body, or if you use a non-constant-time comparison.

What is JWT HS256 and how does it relate to HMAC?

JWT (JSON Web Token) with the HS256 algorithm uses HMAC-SHA256 to sign the token. The JWT structure is: base64url(header) + "." + base64url(payload) + "." + base64url(HMAC-SHA256(secret, base64url(header) + "." + base64url(payload))). The "HS" in HS256 stands for HMAC-SHA256. HS384 uses HMAC-SHA384, HS512 uses HMAC-SHA512. All parties that need to verify the JWT must share the same secret key, which is why HS256 is called a symmetric algorithm. This is appropriate for internal microservices sharing a common secret, but for external parties (third-party APIs), use RS256 (RSA-based asymmetric signing) so you can share the public key without revealing the private key.

How does AWS Signature Version 4 use HMAC-SHA256?

AWS Signature Version 4 (SigV4) uses a chain of HMAC-SHA256 computations to derive a signing key from four inputs: your AWS secret key, the date, the region, and the service name. The derivation is: kDate = HMAC-SHA256("AWS4" + secret_key, date); kRegion = HMAC-SHA256(kDate, region); kService = HMAC-SHA256(kRegion, service); kSigning = HMAC-SHA256(kService, "aws4_request"). The final request signature is then: HMAC-SHA256(kSigning, string_to_sign), where string_to_sign includes the hashed canonical request. This chained derivation means the signing key is specific to the date/region/service combination, limiting the blast radius if a derived key is compromised.

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

🔐HMAC Generator#Hash GeneratorJWTJWT Decoder🔒Bcrypt Hash Generator

Related Articles

온라인 해시 생성기 — MD5, SHA-256, SHA-512: 개발자 완벽 가이드

무료 온라인 해시 생성기. MD5, SHA-1, SHA-256, SHA-512 지원. 해시 알고리즘 원리, MD5 vs SHA-256 비교, bcrypt/Argon2 비밀번호 해싱, HMAC, 블록체인 해싱, JavaScript/Python/Go 코드 예제.

Bcrypt 온라인 생성기: 패스워드 해싱 완전 가이드

bcrypt 패스워드 해싱 작동 원리, 비용 인수, 솔트 라운드, Node.js/Python/PHP에서 bcrypt 사용법. bcrypt vs Argon2 vs scrypt 비교 및 보안 모범 사례.

온라인 JWT 디코더: JSON Web Token 디코딩, 검사 & 디버그 (2026 가이드)

무료 온라인 JWT 디코더로 JWT 헤더, 페이로드, 클레임을 즉시 검사하세요. JWT 구조, 표준 클레임, JavaScript/Python/Go/Java 디코딩, 서명 알고리즘, 보안 모범 사례 포함.

API 인증: OAuth 2.0 vs JWT vs API Key

API 인증 방법 비교: OAuth 2.0, JWT Bearer 토큰, API Key. 각 방법의 사용 시나리오, 보안 절충안, 구현 패턴.