Autenticacao HMAC
Como assinar requisicoes para a API de PIX usando HMAC-SHA256
Toda chamada a POST /api/v1/pix exige assinatura HMAC-SHA256 valida. Chamadas sem assinatura, com assinatura invalida ou fora da janela de tempo sao rejeitadas com HTTP 401/403.
Conceitos
| Termo | Significado |
|---|---|
| Client ID | Identificador publico do chamador (ex: cnab-uau). Vai no header X-Client-Id |
| Secret | Segredo simetrico associado ao Client ID. Nunca trafega na request. Distribuido fora-de-banda (cofre, secret manager) |
| Idempotency-Key | UUID v4 unico por requisicao logica. Tem papel duplo: idempotencia e componente da assinatura |
| String canonica | Texto deterministico construido a partir da requisicao. E o que se assina, nao o body cru |
| Janela de timestamp | Tempo maximo entre o X-Timestamp enviado e o relogio do servidor. Padrao: 2 minutos |
Headers obrigatorios
| Header | Formato | Exemplo |
|---|---|---|
Content-Type | application/json | application/json |
Idempotency-Key | UUID v4 | 550e8400-e29b-41d4-a716-446655440000 |
X-Client-Id | string ASCII | cnab-uau |
X-Timestamp | ISO-8601 UTC com sufixo Z | 2026-04-26T14:30:00Z |
X-Signature | hmac-sha256=<hex-lowercase-64-chars> | hmac-sha256=9f86d081... |
X-Timestampdeve ser UTC (Z), nao usar offset (+00:00ou-03:00)X-Signaturetem prefixo obrigatoriohmac-sha256=. Sem o prefixo a request e rejeitadaIdempotency-Keydeve ser unico por operacao logica. Reusar gera HTTP 409
String canonica
A assinatura e calculada sobre uma string canonica composta por 5 linhas separadas por \n (LF, byte 0x0A) — nunca \r\n:
<METHOD em uppercase>\n
<path absoluto sem query string>\n
<valor de Idempotency-Key>\n
<valor de X-Timestamp>\n
<sha256(body) em hex lowercase>Exemplo
Dado:
- Metodo:
POST - Path:
/api/v1/pix - Idempotency-Key:
550e8400-e29b-41d4-a716-446655440000 - Timestamp:
2026-04-26T14:30:00Z - Body:
{"valor":1.00}
String canonica:
POST
/api/v1/pix
550e8400-e29b-41d4-a716-446655440000
2026-04-26T14:30:00Z
<sha256-hex-do-body>Regras
| Componente | Regra |
|---|---|
| Metodo | Sempre uppercase. POST, nao post |
| Path | /api/v1/pix. Sem query string, sem host, sem trailing slash |
| Idempotency-Key | Mesmo valor enviado no header, byte a byte |
| Timestamp | Mesmo valor enviado no header, byte a byte |
| Body hash | sha256 dos bytes crus do request body, antes de qualquer parse JSON. Hex em lowercase |
| Body vazio | Hash do byte array vazio: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| Separador | \n (LF, 0x0A). Nunca \r\n |
| Trailing newline | Nao colocar newline depois do hash do body |
Passo a passo
- Gerar
Idempotency-Key(UUID v4 novo por requisicao) - Gerar
X-Timestampno formatoYYYY-MM-DDTHH:MM:SSZ(UTC, agora) - Serializar o body para JSON e converter para bytes UTF-8. Guardar esses bytes — sao usados para o hash e para o envio
- Calcular
bodyHash = lowercase(hex(sha256(bodyBytes))) - Montar a string canonica
- Calcular
signatureBytes = HMAC_SHA256(secret, canonicalString) - Codificar
signatureHex = lowercase(hex(signatureBytes)) - Enviar a request com header
X-Signature: hmac-sha256=<signatureHex>
O body enviado no passo 3 tem que ser exatamente o mesmo usado para calcular o hash no passo 4. Se voce reserializar o JSON depois (mudando ordem de chaves, espacamento ou numeros), o hash do servidor nao bate.
Exemplos
Em todos os exemplos, substitua SECRET, o body e a URL conforme seu ambiente. O segredo nunca deve ser hard-coded em codigo de producao — leia de variavel de ambiente, AWS Secrets Manager, GCP Secret Manager ou cofre equivalente.
Bash + openssl
#!/usr/bin/env bash
set -euo pipefail
URL="https://api.example.com/api/v1/pix"
CLIENT_ID="cnab-uau"
SECRET="${CNAB_UAU_HMAC_SECRET:?defina CNAB_UAU_HMAC_SECRET}"
BODY='{"dataLancamento":"2026-04-26","valor":1.00,"tipoChave":"CHAVE_PIX_EMAIL","chavePix":"a@b.com"}'
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
IK=$(uuidgen | tr '[:upper:]' '[:lower:]')
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
CANONICAL=$(printf 'POST\n/api/v1/pix\n%s\n%s\n%s' "$IK" "$TS" "$BODY_HASH")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -sS -X POST "$URL" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IK" \
-H "X-Client-Id: $CLIENT_ID" \
-H "X-Timestamp: $TS" \
-H "X-Signature: hmac-sha256=$SIG" \
--data-raw "$BODY"Python
import hashlib, hmac, json, os, uuid, datetime, urllib.request
URL = "https://api.example.com/api/v1/pix"
CLIENT_ID = "cnab-uau"
SECRET = os.environ["CNAB_UAU_HMAC_SECRET"].encode("utf-8")
payload = {
"dataLancamento": "2026-04-26",
"valor": 1.00,
"tipoChave": "CHAVE_PIX_EMAIL",
"chavePix": "a@b.com",
}
body_bytes = json.dumps(payload, separators=(",", ":")).encode("utf-8")
idempotency_key = str(uuid.uuid4())
timestamp = datetime.datetime.now(
datetime.timezone.utc
).strftime("%Y-%m-%dT%H:%M:%SZ")
body_hash = hashlib.sha256(body_bytes).hexdigest()
canonical = f"POST\n/api/v1/pix\n{idempotency_key}\n{timestamp}\n{body_hash}"
signature = hmac.new(
SECRET, canonical.encode("utf-8"), hashlib.sha256
).hexdigest()
req = urllib.request.Request(
URL,
data=body_bytes,
method="POST",
headers={
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
"X-Client-Id": CLIENT_ID,
"X-Timestamp": timestamp,
"X-Signature": f"hmac-sha256={signature}",
},
)
with urllib.request.urlopen(req) as resp:
print(resp.status, resp.read().decode())Cuidado com json.dumps do Python: o exemplo usa separators=(",", ":") para gerar JSON compacto e deterministico. Se voce usar indent=... ou outras opcoes, os bytes mudam.
Node.js
import crypto from "node:crypto";
import { randomUUID } from "node:crypto";
const URL_API = "https://api.example.com/api/v1/pix";
const CLIENT_ID = "cnab-uau";
const SECRET = process.env.CNAB_UAU_HMAC_SECRET;
const payload = {
dataLancamento: "2026-04-26",
valor: 1.0,
tipoChave: "CHAVE_PIX_EMAIL",
chavePix: "a@b.com",
};
const bodyString = JSON.stringify(payload);
const idempotencyKey = randomUUID();
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
const bodyHash = crypto
.createHash("sha256")
.update(bodyString, "utf8")
.digest("hex");
const canonical = `POST\n/api/v1/pix\n${idempotencyKey}\n${timestamp}\n${bodyHash}`;
const signature = crypto
.createHmac("sha256", SECRET)
.update(canonical, "utf8")
.digest("hex");
const resp = await fetch(URL_API, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
"X-Client-Id": CLIENT_ID,
"X-Timestamp": timestamp,
"X-Signature": `hmac-sha256=${signature}`,
},
body: bodyString,
});
console.log(resp.status, await resp.text());Java (JDK 11+)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.HexFormat;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class PixClientExample {
public static void main(String[] args) throws Exception {
String url = "https://api.example.com/api/v1/pix";
String clientId = "cnab-uau";
String secret = System.getenv("CNAB_UAU_HMAC_SECRET");
String body = "{\"dataLancamento\":\"2026-04-26\",\"valor\":1.00,"
+ "\"tipoChave\":\"CHAVE_PIX_EMAIL\",\"chavePix\":\"a@b.com\"}";
byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
String idempotencyKey = UUID.randomUUID().toString();
String timestamp = DateTimeFormatter.ISO_INSTANT
.format(Instant.now().truncatedTo(
java.time.temporal.ChronoUnit.SECONDS));
HexFormat hex = HexFormat.of();
String bodyHash = hex.formatHex(
MessageDigest.getInstance("SHA-256").digest(bodyBytes));
String canonical = "POST\n/api/v1/pix\n"
+ idempotencyKey + "\n" + timestamp + "\n" + bodyHash;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = hex.formatHex(
mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json")
.header("Idempotency-Key", idempotencyKey)
.header("X-Client-Id", clientId)
.header("X-Timestamp", timestamp)
.header("X-Signature", "hmac-sha256=" + signature)
.POST(HttpRequest.BodyPublishers.ofByteArray(bodyBytes))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}Postman (Pre-request Script)
Cole no tab Pre-request Script da request:
const secret = pm.environment.get("CNAB_UAU_HMAC_SECRET");
const clientId = pm.environment.get("CNAB_CLIENT_ID") || "cnab-uau";
const idempotencyKey = crypto.randomUUID();
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
const bodyRaw =
pm.request.body && pm.request.body.raw ? pm.request.body.raw : "";
const bodyHash = CryptoJS.SHA256(bodyRaw).toString(CryptoJS.enc.Hex);
const canonical = `POST\n/api/v1/pix\n${idempotencyKey}\n${timestamp}\n${bodyHash}`;
const signature = CryptoJS.HmacSHA256(canonical, secret).toString(
CryptoJS.enc.Hex,
);
pm.request.headers.upsert({ key: "Idempotency-Key", value: idempotencyKey });
pm.request.headers.upsert({ key: "X-Client-Id", value: clientId });
pm.request.headers.upsert({ key: "X-Timestamp", value: timestamp });
pm.request.headers.upsert({
key: "X-Signature",
value: `hmac-sha256=${signature}`,
});Validacoes no servidor
As validacoes sao executadas na seguinte ordem:
- Existencia de todos os headers obrigatorios (401
MISSING_HEADER) X-Client-Idconhecido (403FORBIDDEN)X-Signaturecomeca comhmac-sha256=(401INVALID_SIGNATURE_FORMAT)X-Timestampparseavel como ISO-8601 (401TIMESTAMP_INVALID)- Diferenca entre
noweX-Timestampdentro de 2 min (401TIMESTAMP_EXPIRED) - Body ate 1 MB (401
BODY_TOO_LARGE) - HMAC recalculado bate com
X-Signatureem comparacao constant-time (401SIGNATURE_MISMATCH) Idempotency-Keyainda nao usada (409CONFLICT)
Em caso de falha o servidor responde com mensagem generica "Autenticacao invalida" — o motivo exato fica apenas em logs e metricas (hmac_auth_failures_total), para nao dar pista a atacante.
Codigos de erro
| HTTP | Codigo | Quando acontece | Acao do cliente |
|---|---|---|---|
| 401 | UNAUTHORIZED | Headers HMAC ausentes, timestamp expirado/invalido, assinatura nao bate, body > 1MB | Verificar implementacao |
| 403 | FORBIDDEN | X-Client-Id desconhecido | Verificar valor ou pedir provisionamento |
| 409 | CONFLICT | Idempotency-Key ja usada | Usar chave nova para nova operacao |
Toda resposta de erro inclui traceId para correlacao:
{
"code": "UNAUTHORIZED",
"message": "Autenticacao invalida",
"traceId": "abc123def456",
"timestamp": "2026-04-26T14:30:01.234Z"
}FAQ
Posso reaproveitar a assinatura ao retentar uma chamada que deu timeout?
Sim, se ainda estiver dentro da janela de timestamp (2 min) e usar a mesma Idempotency-Key, mesmo body e mesmo X-Timestamp. O servidor reconhece a chave e devolve resposta idempotente, ou processa pela primeira vez se nunca chegou antes.
Por que Idempotency-Key faz parte da assinatura?
Para impedir que um atacante reutilize uma assinatura valida com Idempotency-Key diferente para forcar nova execucao. Como a chave entra na canonica, mudar a chave invalida a assinatura.
Posso usar offset (-03:00) no timestamp?
Nao. O servidor usa Instant.parse(), que aceita apenas Z (UTC). Sempre converta para UTC antes de formatar.
Como rotacionar o segredo?
Atualmente: redeploy com novo valor da variavel de ambiente. Coordenacao necessaria com o cliente.
Troubleshooting
| Sintoma | Causa provavel | Como corrigir |
|---|---|---|
| Sempre 401 mesmo com tudo certo | Reserializacao do body entre calculo do hash e envio | Logar o byte array exato usado em ambos os pontos |
| 401 ocasional em horarios de pico | Drift de relogio | Verificar NTP no host do cliente |
| 401 so atras de proxy | Proxy reescreve path/method | Confirmar que o servidor recebe POST /api/v1/pix |
403 FORBIDDEN | X-Client-Id nao cadastrado no ambiente | Validar valor; pedir provisionamento |
409 CONFLICT em retry | Idempotency-Key ja usada com sucesso | Gerar chave nova para nova operacao |
| Funciona em curl, falha em Postman | Postman re-formatando o body | Usar pre-request script e conferir pm.request.body.raw |
Inclua o traceId da resposta de erro em qualquer abertura de chamado — ele permite correlacionar com os logs do servidor.
Checklist do cliente
- Segredo lido de variavel de ambiente / secret manager (nunca hard-coded)
- Segredo nunca aparece em logs nem em mensagens de erro
-
Idempotency-Keye UUID v4 novo a cada operacao logica -
X-Timestampem UTC com sufixoZ, sem milissegundos, gerado no momento do envio - Mesma string de body usada para hash e para envio (sem reserializacao)
- Hash hex em lowercase
- Separador da canonica e
\n, nao\r\n - Sem trailing newline no fim da canonica
- Em 5xx ou timeout: reusar a mesma
Idempotency-Key. Em 2xx ou 4xx: gerar nova chave - Relogio do host sincronizado via NTP (drift < 30s)
- Comunicacao so por HTTPS
