digest.hmac_sha256

STRINGdigest.hmac_sha256STRINGkeySTRINGs

Available inall subroutines.

Returns the HMAC-SHA256 of message using key, as a lowercase hexadecimal string with a 0x prefix.

Parameters

ParameterTypeDescription
keySTRINGThe secret key for HMAC computation
messageSTRINGThe message to authenticate

The key is used directly as the HMAC key. For keys longer than 64 bytes (the SHA-256 block size), the key is first hashed with SHA-256 before use, as specified in RFC 2104.

The message can be any string, including an empty string. Multiple strings can be concatenated using the + operator before passing to the function.

Return value

Returns a 66-character string: a 0x prefix followed by 64 lowercase hexadecimal characters representing the 256-bit (32-byte) HMAC.

Example output: 0x095d5a21fe6d0646db223fdf3de6436bb8dfb2fab0b51677ecf6441fcf5f2a67

If key is empty or not set, the function returns an empty string (not set).

Examples

Basic usage

declare local var.hmac STRING;
set var.hmac = digest.hmac_sha256("secret-key", "hello world");
# Result: 0x095d5a21fe6d0646db223fdf3de6436bb8dfb2fab0b51677ecf6441fcf5f2a67

To verify this output using OpenSSL:

$ echo -n "hello world" | openssl dgst -sha256 -hmac "secret-key"
SHA2-256(stdin)= 095d5a21fe6d0646db223fdf3de6436bb8dfb2fab0b51677ecf6441fcf5f2a67

Signing a request

A common use case is signing API requests to prove authenticity:

sub vcl_recv {
declare local var.string_to_sign STRING;
declare local var.signature STRING;
declare local var.secret_key STRING;
set var.secret_key = table.lookup(api_keys, "webhook-secret");
if (var.secret_key == "") {
error 500 "Signing key not configured";
}
# Build the string to sign from request components
set var.string_to_sign = req.method + "\n"
+ req.url.path + "\n"
+ req.http.Host + "\n"
+ now.sec;
set var.signature = digest.hmac_sha256(var.secret_key, var.string_to_sign);
# Add signature to request for backend verification
set req.http.X-Signature = var.signature;
set req.http.X-Timestamp = now.sec;
}

Verifying a webhook signature

When receiving webhooks, verify the signature matches:

sub vcl_recv {
declare local var.expected_sig STRING;
declare local var.secret_key STRING;
declare local var.body STRING;
set var.secret_key = table.lookup(webhook_secrets, "github");
if (var.secret_key == "") {
error 500 "Webhook secret not configured";
}
# Read the request body
set var.body = req.body;
# Compute expected signature
set var.expected_sig = digest.hmac_sha256(var.secret_key, var.body);
# Compare using constant-time comparison to prevent timing attacks
if (!digest.secure_is_equal(var.expected_sig, req.http.X-Hub-Signature-256)) {
error 401 "Invalid signature";
}
}

Using string concatenation

The message parameter accepts concatenated strings:

declare local var.hmac STRING;
# These produce the same result
set var.hmac = digest.hmac_sha256("key", "hello world");
set var.hmac = digest.hmac_sha256("key", "hello" + " " + "world");

Deriving keys for encryption

HMAC-SHA256 can be used to derive encryption keys from a master secret:

sub derive_key(STRING var.master_key, STRING var.context) STRING {
return digest.hmac_sha256(var.master_key, "encryption-key|" + var.context);
}
declare local var.encryption_key STRING;
set var.encryption_key = derive_key(table.lookup(secrets, "master"), client.ip);

Security considerations

Key management

  • Store keys in edge dictionaries, not in VCL source code.
  • Use keys of at least 32 bytes (256 bits) for security equivalent to SHA-256.
  • Rotate keys periodically and have a process for key compromise.

Constant-time comparison

When comparing HMAC values for authentication, always use digest.secure_is_equal to prevent timing attacks. String comparison with == leaks information about which bytes matched, potentially allowing an attacker to forge valid authentication tags:

# WRONG - vulnerable to timing attacks
if (var.computed_hmac == req.http.X-Signature) { ... }
# CORRECT - constant-time comparison
if (digest.secure_is_equal(var.computed_hmac, req.http.X-Signature)) { ... }

Empty keys

If the key is empty or not set, the function returns an empty string. Always validate that key lookups succeed before computing an HMAC:

declare local var.key STRING;
set var.key = table.lookup(secrets, "api-key");
if (var.key == "") {
error 500 "API key not found";
}
set var.hmac = digest.hmac_sha256(var.key, var.message);