Skip to content

Signer et vérifier avec HMAC-SHA256 snippet

HMAC boulonne une clé secrète sur un hash — la façon standard de prouver qu'un message vient de quelqu'un qui détient la clé (signatures de webhook, auth d'API).

HMAC boulonne une clé secrète sur un hash — la façon standard de prouver qu'un message vient de quelqu'un qui détient la clé (signatures de webhook, auth d'API). Deux règles sont absolues : ne jamais l'écrire à la main en sha256(clé + message), qui cède aux attaques par extension de longueur, et ne jamais vérifier avec une simple égalité de chaînes, qui fait fuiter par timing combien d'octets de tête correspondent. Chaque langage ci-dessous fournit une comparaison en temps constant — sers-t'en. SQL est omis (il faut pgcrypto) ; C et C++ aussi (pas de crypto en stdlib).

Recette exécutable · 12 langagesOpen the hmac-generator tool →
Crypto & Encodinghmacsignaturewebhooksauthenticationcrypto

Every language

12 langages, 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 →