HMAC-SHA512
Toutes les requêtes POST sur /api/external/* (cash-in, cash-out, refund, webhooks, validation CPF) exigent une signature HMAC-SHA512 du body pour garantir l'intégrité et l'authenticité du payload.
Portée
- Uniquement
POST. Les requêtesGETetDELETEn'ont pas besoin de l'en-têtehmac(le plug n'est pas dans leur pipeline). - Endpoints couverts :
POST /pix/cash-in,POST /pix/cash-out,POST /pix/refund,POST /webhooks,POST /cpf/validate. - Header obligatoire :
hmacen hexadécimal lowercase (l'API normalise en lowercase avant de comparer, mais envoyez toujours en lowercase pour éviter toute ambiguïté).
Comment ça fonctionne
- Sérialisez le body de la requête en JSON sans indentation (compact) avec les clés en ordre alphabétique
- Générez la signature HMAC-SHA512 en utilisant votre
client_secretcomme clé - Envoyez la signature dans l'en-tête
hmacen format hexadécimal lowercase - Envoyez le même body (exactement comme il a été signé) dans le corps de la requête
L'API recalcule le hash sur le body reçu (après normalisation interne) et le compare à la valeur de l'en-tête hmac en utilisant une comparaison en temps constant (constant-time comparison), empêchant les attaques par timing qui tentent de découvrir la signature octet par octet.
Normalisation du body (critique)
Le serveur normalise le body reçu avant de calculer le HMAC. Pour que la signature soit valide, le client doit signer le body exactement dans la forme normalisée que le serveur utilise :
JSON.parsedu body reçu- Re-sérialiser avec les clés en ordre alphabétique (le serveur réordonne les clés avant de calculer le HMAC attendu)
JSON.stringifysans indentation (équivalent àjson.dumps(obj, sort_keys=True, separators=(',', ':'))en Python)- Supprimer 1 espace qui existe après chaque
:ou,(convertit", "en","et": "en":"). Défensif --JSON.stringify(obj)etjson.dumps(obj, separators=(',', ':'))ne produisent jamais ces espaces ; les sérialiseurs qui émettent", "entre paires (ex. :json.dumpspar défaut en Python) sont automatiquement compatibles - Calculer
HMAC-SHA512(body_normalizé, client_secret)en hexadécimal lowercase - Envoyer le même body normalisé (avec clés triées alphabétiquement, sans espaces après
:et,) comme request body et le HMAC dans l'en-têtehmac
L'ordre alphabétique des clés est OBLIGATOIRE
Le serveur réordonne les clés du JSON en ordre alphabétique avant de calculer le HMAC attendu. Si le client signe le body dans un autre ordre, la signature ne correspond pas et la requête est rejetée avec HTTP 401 Invalid HMAC signature.
Exemple concret :
- Le client envoie
{"external_id":"...","amount":1000}et signe exactement cette chaîne - Le serveur réordonne en interne vers
{"amount":1000,"external_id":"..."}avant validation - Les HMAC diffèrent → requête rejetée
Solution : triez les clés alphabétiquement avant de sérialiser et de signer, et envoyez exactement cette même chaîne triée dans le corps de la requête.
Bibliothèques qui NE trient PAS par défaut
Si vous utilisez Java Jackson, C# System.Text.Json ou PHP json_encode, l'ordre des clés n'est pas alphabétique par défaut. Vous devez forcer le tri.
- Java (Jackson) : utilisez
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)ou sérialisez à partir d'unTreeMap<String, Object>(qui maintient les clés triées). - C# :
JsonSerializerOptionsne trie pas ; triez viaSortedDictionary<string, object>avant de sérialiser. - PHP : appelez
ksort($array)avantjson_encode($array). - Python :
json.dumps(obj, sort_keys=True, separators=(',', ':'))— natif, sans configuration supplémentaire. - Go :
json.Marshald'unemap[string]interface{}garantit déjà l'ordre alphabétique depuis Go 1.12+. - JavaScript/Node :
JSON.stringifypréserve l'ordre d'insertion ; triez les clés manuellement avant de sérialiser :javascriptconst sorted = Object.keys(obj).sort().reduce((acc, key) => { acc[key] = obj[key]; return acc; }, {}); const body = JSON.stringify(sorted);
Attention à JSON.stringify(obj, null, 2)
JSON.stringify(obj) (sans indentation) produit {"a":1,"b":2} — sans espaces, déjà normalisé.
JSON.stringify(obj, null, 2) (avec indentation pour humains) produit :
{
"a": 1,
"b": 2
}— avec espaces après : et retours à la ligne. Cela casse le HMAC. Utilisez toujours JSON.stringify(obj) sans indentation pour calculer et envoyer le HMAC.
HMAC-SHA512 vs Webhook SHA256
L'API utilise HMAC-SHA512 pour authentifier les requêtes que vous envoyez. Les webhooks envoyés par Owem Pay utilisent HMAC-SHA256 pour la signature (en-tête X-Owem-Signature). Ce sont des algorithmes différents -- chacun dans son contexte. Voir Webhooks pour les détails de la validation de webhooks.
Autre différence importante : dans le request HMAC-SHA512 (sens client → Owem) le serveur réordonne alphabétiquement les clés avant de comparer, donc le client doit produire le body trié. Dans le webhook HMAC-SHA256 (sens Owem → client) le client reçoit le body exactement tel qu'il a été envoyé (sans réordonnancement ultérieur) et doit valider la signature sur ce body RAW -- ne resérialisez pas avant de valider.
Headers obligatoires
| Header | Valeur | Notes |
|---|---|---|
Authorization | ApiKey {client_id}:{client_secret} ou Basic {base64(client_id:client_secret)} | Même credential, deux formats acceptés |
Content-Type | application/json | POST sans Content-Type: application/json → 415 avant même le HMAC |
hmac | Signature HMAC-SHA512 en hexadécimal lowercase | 128 caractères hex. Si envoyé en majuscules, le serveur normalise en lowercase avant de comparer -- mais envoyez déjà en lowercase par convention |
Exemples
JavaScript (Node.js)
const crypto = require('crypto');
// Keys en ordre alphabétique (amount < description < pix_key < pix_key_type)
const payload = {
amount: 3000,
description: "Paiement",
pix_key: "12345678901",
pix_key_type: "cpf"
};
// JSON.stringify préserve l'ordre d'insertion, NE trie PAS par clé.
// Même si l'objet ci-dessus est alphabétique, forcer un sort explicite
// garantit que le body correspond au hash attendu par le serveur.
const sortedKeys = Object.keys(payload).sort();
const body = JSON.stringify(
sortedKeys.reduce((acc, key) => ({ ...acc, [key]: payload[key] }), {})
);
// body = '{"amount":3000,"description":"Paiement","pix_key":"12345678901","pix_key_type":"cpf"}'
const hmac = crypto
.createHmac('sha512', 'sk_votre-client-secret')
.update(body)
.digest('hex');
// Envoyer `body` comme request body et `hmac` dans l'en-tête hmac.Python
import hmac
import hashlib
import json
# sort_keys=True garantit l'ordre alphabétique — OBLIGATOIRE
body = json.dumps(
{
"amount": 3000,
"pix_key": "12345678901",
"pix_key_type": "cpf",
"description": "Paiement"
},
sort_keys=True,
separators=(',', ':')
)
# body = '{"amount":3000,"description":"Paiement","pix_key":"12345678901","pix_key_type":"cpf"}'
signature = hmac.new(
b"sk_votre-client-secret",
body.encode("utf-8"),
hashlib.sha512
).hexdigest()PHP
$data = [
'amount' => 3000,
'pix_key' => '12345678901',
'pix_key_type' => 'cpf',
'description' => 'Paiement'
];
// ksort garantit l'ordre alphabétique des clés — OBLIGATOIRE
ksort($data);
$body = json_encode($data, JSON_UNESCAPED_SLASHES);
$hmac = hash_hmac('sha512', $body, 'sk_votre-client-secret');Java
import java.util.TreeMap;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
// TreeMap maintient les clés en ordre alphabétique — OBLIGATOIRE
TreeMap<String, Object> bodyMap = new TreeMap<>();
bodyMap.put("amount", 3000);
bodyMap.put("pix_key", "12345678901");
bodyMap.put("pix_key_type", "cpf");
bodyMap.put("description", "Paiement");
ObjectMapper mapper = new ObjectMapper();
String body = mapper.writeValueAsString(bodyMap);
// body = {"amount":3000,"description":"Paiement","pix_key":"12345678901","pix_key_type":"cpf"}
String secret = "sk_votre-client-secret";
Mac mac = Mac.getInstance("HmacSHA512");
SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA512");
mac.init(keySpec);
byte[] hash = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
String hmac = sb.toString();C#
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
// SortedDictionary maintient les clés en ordre alphabétique — OBLIGATOIRE
// (JsonSerializerOptions NE trie PAS les clés par défaut)
var payload = new SortedDictionary<string, object>
{
["amount"] = 3000,
["description"] = "Paiement",
["pix_key"] = "12345678901",
["pix_key_type"] = "cpf"
};
var body = JsonSerializer.Serialize(payload);
// body = {"amount":3000,"description":"Paiement","pix_key":"12345678901","pix_key_type":"cpf"}
var secret = Encoding.UTF8.GetBytes("sk_votre-client-secret");
using var hmacSha512 = new HMACSHA512(secret);
var hash = hmacSha512.ComputeHash(Encoding.UTF8.GetBytes(body));
var hmac = BitConverter.ToString(hash).Replace("-", "").ToLower();Bash (curl)
BODY='{"amount":3000,"description":"Paiement","pix_key":"12345678901","pix_key_type":"cpf"}'
SECRET="sk_votre-client-secret"
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://api.owem.com.br/api/external/pix/cash-out \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-d "$BODY"Validation
L'API recalcule le HMAC-SHA512 du body reçu (après normalisation) et le compare à la valeur reçue dans l'en-tête hmac en utilisant une comparaison en temps constant. Toutes les erreurs du plug HmacValidation suivent le format {"worked": false, "detail": "..."} :
401 -- Signature invalide ou header absent
{
"worked": false,
"detail": "Invalid HMAC signature"
}{
"worked": false,
"detail": "Missing HMAC header"
}400 -- Body absent ou JSON invalide
{
"worked": false,
"detail": "Request body is required for HMAC validation"
}{
"worked": false,
"detail": "Request body must be valid JSON for HMAC validation"
}403 -- API Key sans HMAC secret configuré
{
"worked": false,
"detail": "HMAC secret not configured for this API key"
}Checklist d'intégration HMAC
- Utilisez le body exactement tel qu'envoyé dans la requête (même sérialisation JSON, clés en ordre alphabétique, sans espaces après
:et,). - Le
client_secretde l'API Key est la clé HMAC -- la même valeur envoyée dansAuthorization: ApiKey {client_id}:{client_secret}(ou à l'intérieur dubase64quand vous utilisezAuthorization: Basic). - La signature doit avoir 128 caractères hexadécimaux en lowercase (SHA-512 produit 64 octets = 128 caractères hex). L'API normalise l'en-tête en lowercase avant de comparer, mais envoyez déjà en lowercase par convention. Les majuscules ou le mixte (ex. :
AbCdEf) fonctionnent mais sont déconseillés. - La comparaison est faite en temps constant -- il n'y a aucun moyen de découvrir la signature par timing.
- Si votre client envoie
X-Key-Case: camelCase, signez le body dans la forme qui transite sur le réseau (camelCase). Le HMAC voit toujours le body tel que vous l'avez envoyé, avant toute conversion interne -- voir Authentification -- Headers optionnels.