Skip to content

Firmar y verificar con HMAC-SHA256 snippet

HMAC atornilla una clave secreta a un hash — la forma estándar de demostrar que un mensaje vino de alguien que posee la clave (firmas de webhooks, autenticación de APIs).

HMAC atornilla una clave secreta a un hash — la forma estándar de demostrar que un mensaje vino de alguien que posee la clave (firmas de webhooks, autenticación de APIs). Dos reglas son absolutas: nunca lo implementes a mano como sha256(clave + mensaje), que cae ante ataques de extensión de longitud, y nunca verifiques con igualdad de cadenas ordinaria, que filtra por timing cuántos bytes iniciales coincidieron. Todos los lenguajes de abajo traen una comparación en tiempo constante — úsala. SQL queda fuera (necesita pgcrypto); también C y C++ (sin crypto en la stdlib).

Receta ejecutable · 12 lenguajesAbrir la herramienta hmac-generator →
Crypto & Encodinghmacsignaturewebhooksauthenticationcrypto

Every language

12 lenguajes, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
const enc = new TextEncoder();

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

const mac = await crypto.subtle.sign('HMAC', key, enc.encode('message'));
const hex = [...new Uint8Array(mac)]
  .map((b) => b.toString(16).padStart(2, '0'))
  .join('');

Node's sync one-liner: crypto.createHmac('sha256', 'secret').update('message').digest('hex'). Verification in Node: crypto.timingSafeEqual — browsers have no constant-time compare, compare server-side.

TSTypeScript
async function hmacSha256Hex(secret: string, message: string): Promise<string> {
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    enc.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const mac = await crypto.subtle.sign('HMAC', key, enc.encode(message));
  return [...new Uint8Array(mac)]
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

Webhook verification in Node: crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)) — a === on MACs is a timing leak.

GoGo
mac := hmac.New(sha256.New, []byte("secret"))
mac.Write([]byte("message"))
sum := mac.Sum(nil)
hexStr := hex.EncodeToString(sum)

// verify — constant time, never bytes.Equal or ==
expected, _ := hex.DecodeString(hexStr)
ok := hmac.Equal(sum, expected)
fmt.Println(hexStr, ok)

hmac.Equal runs in constant time; bytes.Equal returns at the first mismatch and leaks the common prefix length through response timing.

RsRust
use hmac::{Hmac, Mac};
use sha2::Sha256;

type HmacSha256 = Hmac<Sha256>;

fn main() {
    // sign
    let mut mac = HmacSha256::new_from_slice(b"secret").unwrap();
    mac.update(b"message");
    let tag = mac.finalize().into_bytes();

    // verify — constant-time inside verify_slice
    let mut verifier = HmacSha256::new_from_slice(b"secret").unwrap();
    verifier.update(b"message");
    verifier.verify_slice(&tag).expect("tag mismatch");
}

verify_slice returns Err on any mismatch without early exit — the RustCrypto crates bake the constant-time discipline in. new_from_slice accepts any key length.

PHPPHP
$hex = hash_hmac('sha256', 'message', 'secret');

// webhook-style verification
$expected = hash_hmac('sha256', $receivedPayload, $secret);
if (!hash_equals($expected, $receivedHex)) {
    throw new RuntimeException('bad signature');
}

hash_equals is the constant-time compare PHP ships for exactly this. Note the argument order: known value first.

PyPython
import hashlib
import hmac

mac = hmac.new(b'secret', b'message', hashlib.sha256).hexdigest()

# constant-time verification
ok = hmac.compare_digest(mac, expected_hex)

Use the hmac module, never hashlib.sha256(key + msg) — raw concatenation falls to length-extension attacks. compare_digest accepts str or bytes.

C#C#
using System.Security.Cryptography;

byte[] Tag(string secret, string message) =>
    HMACSHA256.HashData(
        Encoding.UTF8.GetBytes(secret),
        Encoding.UTF8.GetBytes(message));

bool Verify(string secret, string message, byte[] expected) =>
    CryptographicOperations.FixedTimeEquals(
        Tag(secret, message), expected);

HMACSHA256.HashData (.NET 7+) is the one-shot sign. CryptographicOperations.FixedTimeEquals is the constant-time compare — LINQ's SequenceEqual leaks.

JvJava
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(
        "secret".getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] tag = mac.doFinal("message".getBytes(StandardCharsets.UTF_8));

String hex = HexFormat.of().formatHex(tag);

// constant-time verify
boolean ok = MessageDigest.isEqual(tag, expectedBytes);

The algorithm string is "HmacSHA256" — passing "SHA-256" throws a NoSuchAlgorithmException. MessageDigest.isEqual is the constant-time compare.

SwSwift
import CryptoKit

let key = SymmetricKey(data: Data("secret".utf8))

let mac = HMAC<SHA256>.authenticationCode(
    for: Data("message".utf8), using: key)
let hex = mac.map { String(format: "%02x", $0) }.joined()

// constant-time verification built in
let ok = HMAC<SHA256>.isValidAuthenticationCode(
    Data(hex: hex),
    authenticating: Data("message".utf8),
    using: key)

SymmetricKey wraps the secret as bytes, not a string. isValidAuthenticationCode never early-exits — CryptoKit made the safe path the easy path.

KtKotlin
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.security.MessageDigest
import java.util.HexFormat

fun hmacSha256Hex(secret: String, message: String): String {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(secret.toByteArray(), "HmacSHA256"))
    return HexFormat.of().formatHex(mac.doFinal(message.toByteArray()))
}

fun verify(secret: String, message: String, expectedHex: String): Boolean {
    val expected = HexFormat.of().parseHex(expectedHex)
    return MessageDigest.isEqual( // constant-time
        HexFormat.of().parseHex(hmacSha256Hex(secret, message)), expected)
}

Same JVM primitives as Java — the Kotlin win is the two clean functions instead of the initialized-once pattern.

RbRuby
require 'openssl'

mac = OpenSSL::HMAC.hexdigest('SHA256', 'secret', 'message')

# constant-time verification
ok = OpenSSL.secure_compare(mac, received_hex)

OpenSSL.secure_compare (active_support adds ActiveSupport::SecurityUtils.secure_compare on top) — plain == leaks.

ZigZig
const std = @import("std");

pub fn main() !void {
    const HmacSha256 = std.crypto.auth.hmac.sha2.HmacSha256;

    var out: [HmacSha256.mac_length]u8 = undefined;
    HmacSha256.create(&out, "message", "secret"); // one-shot sign

    std.debug.print("{s}\n", .{std.fmt.fmtSliceHexLower(&out)});

    // verify — constant-time internally
    const ok = HmacSha256.verify(&out, "message", "secret");
    std.debug.print("verified: {}\n", .{ok});
}

create/verify are the one-shot pair; init/update/final handle streaming. std.crypto is Monocypher-derived and always constant-time where it matters.

Keep going

Try the interactive hmac-generator tool →