Geração de assinatura HMAC

Geração de assinatura HMAC

Todos os eventos enviados pelos webhooks do Banco Liquidante possuem uma assinatura HMAC.

A assinatura HMAC é um algoritmo que assina as mensagens, assegurando a procedência dos eventos enviados e protegendo o endpoint do parceiro contra ataques de aplicações maliciosas.

Para validar a mensagem recebida, o parceiro também deverá gerar uma assinatura HMAC com o mesmo algoritmo enviado pelo Banco Liquidante.

❗️

Atenção

Ao implementar a assinatura HMAC, o parceiro garante a segurança de sua aplicação.


Etapas

Recebimento dos headers

A aplicação cliente receberá mensagens via chamadas HTTP enviadas pela plataforma do Banco Liquidante.

Nessas chamadas, os seguintes headers são enviados para que a aplicação cliente realize a geração da assinatura HMAC:

HeaderDescriçãoValor de exemplo
authorizationContém a assinatura HMAC. Esse é o valor que deverá ser utilizado pela aplicação do cliente para validar se a mensagem recebida é legítima. O valor é uma HMACSHA256 em base64.hmac lBywDt0rKwAaJHz3cKxzFw6ptdQHrelLE7kbGAbLZ5A=
publicKeyContém a chave pública registrada na plataforma de webhooks do Banco Liquidante para o evento que está sendo enviado. O valor é uma string em base64.NWUyNjgwZDMtNmE2Ni00YWYzLWJkNjUtMGM2ODMzYzczYzI1
nonceContém um valor único e aleatório gerado por requisição, o que aumenta a segurança do algoritmo. O valor é um UUID sem hífens.972004b06b6b443d8ed71630c9430048
requestTimestampContém o Unix Timestamp do momento em que a plataforma do Banco Liquidante efetuou a requisição.1615331979
idempotency-KeyContém a chave de idempotência que identifica a comunicação com o parceiro. A aplicação cliente deverá utilizá-la para garantir que uma mesma requisição não seja processada duas vezes. Essa chave não faz parte da assinatura HMAC.30811733-2b04-44c3-848d-bfbe2976e480

Concatenação de dados

Para construir a assinatura HMAC, a aplicação cliente deve concatenar as informações recebidas nos headers em uma única string, na seguinte ordem:

  • publicKey: chave pública (recomenda-se gerar uma por evento);
  • requestUri: URI em que a aplicação está recebendo a mensagem do webhook, codificada (percent-encode) e em letras minúsculas;
  • requestTimestamp: data e horário em que o Banco Liquidante efetuou a requisição;
  • nonce: valor aleatório gerado para cada requisição;
  • requestBodyBase64: corpo da requisição em formato raw convertido para base64 antes de ser concatenado.
⚠️

Importante

Os campos devem seguir exatamente a ordem apresentada acima e utilizar o separador & entre cada valor.

Exemplo de concatenação

const preHashedString = `${publicKey}&${requestUri}&${requestTimestamp}&${nonce}&${requestBodyBase64}`

Algoritmo de hash

O algoritmo criptográfico utilizado nos webhooks do Banco Liquidante é o SHA-256, que produz hashes irreversíveis e exclusivos de 256 bits.


Exemplo de geração de assinatura HMAC

A seguir, um exemplo completo de geração de assinatura a partir de uma chamada HTTP enviada pela plataforma Banco Liquidante.

1. Considere que a requisição HTTP POST é feita para a seguinte URI:

https://url.com.br/api/webhooks

2. O Banco Liquidante enviará os seguintes valores nos headers:

HeaderValor
requestUrihttps://url.com.br/api/webhooks
publicKeyNWUyNjgwZDMtNmE2Ni00YWYzLWJkNjUtMGM2ODMzYzczYzI1
requestTimestamp1615331979
nonce972004b06b6b443d8ed71630c9430048

3. No body da requisição, será enviado o seguinte payload:

[
  {
    "entityId": "3c92b629-fc89-4d01-9147-48fc5e74d8d0",
    "companyKey": "ACompanyKey",
    "name": "transaction.hold.was.approved",
    "timestamp": "2021-03-09T23:22:00",
    "correlationId": "188be708-ff40-48af-b9c1-1ed9e335ad99",
    "metadata": { "Foo": "Bar" },
    "data": "{\"Bar\":\"31\"}"
  }
]
📘

Nota

O payload é um array. A aplicação endpoint do cliente deve percorrer todo esse array ao processar a mensagem.

A plataforma de webhooks do Banco Liquidante efetuará uma requisição como esta:

curl --location --request POST 'https://url.com.br/api/webhooks' \
--header 'Authorization: hmac To/SnQDQxo3mmuppqcjycxEfI7jAKREpCKFVy0IikjY=' \
--header 'Nonce: 972004b06b6b443d8ed71630c9430048' \
--header 'PublicKey: NWUyNjgwZDMtNmE2Ni00YWYzLWJkNjUtMGM2ODMzYzczYzI1' \
--header 'RequestTimestamp: 1615331979' \
--header 'Content-Type: application/json' \
--data-raw '[{"entityId":"3c92b629-fc89-4d01-9147-48fc5e74d8d0","companyKey":"ACompanyKey","name":"transaction.hold.was.approved","timestamp":"2021-03-09T23:22:00","correlationId":"188be708-ff40-48af-b9c1-1ed9e335ad99","metadata":{"Foo":"Bar"},"data":"{\"Bar\":\"31\"}"}]'

4. Ao receber a requisição, o parceiro concatena todos os valores em uma string:

String antes da geração do HMAC

const separator = "&";
const preHashedString = `${publicKey}${separator}${requestUri}${separator}${requestTimestamp}${separator}${nonce}${separator}${requestBodyBase64}`

// Resultado:
// NWUyNjgwZDMtNmE2Ni00YWYzLWJkNjUtMGM2ODMzYzczYzI1&https%3a%2f%2furl.com.br%2fapi%2fwebhooks&1615331979&972004b06b6b443d8ed71630c9430048&W3siZW50aXR5SWQiOi...

5. Com a string montada, o parceiro gera a assinatura HMAC em base64, utilizando:

  • privateKey: chave privada idêntica à informada na configuração do webhooknão deve ser passada em base64;
  • preHashedString: string do passo anterior, em formato UTF-8.

Exemplo de geração da assinatura

const signature = crypto
  .createHmac('sha256', new Buffer.from(privateKey, 'base64').toString())
  .update(preHashedString, 'utf8')
  .digest('base64')

console.log(signature);
// x7QO2Q4SBVtLFnCO4MJWDr76qm049hjuc18zXZCBbto=
⚠️

Importante

A privateKey nunca é enviada nas requisições pelo Banco Liquidante. Ela deve ser armazenada pelo parceiro de forma segura e utilizada somente no momento da geração do hash HMAC.

6. A assinatura gerada pelo parceiro deve ser idêntica ao valor do header authorization enviado pelo Banco Liquidante.

❗️

Atenção

Somente se as duas assinaturas forem idênticas, o parceiro deve aceitar a mensagem. Caso contrário, deve retornar o status code 401 para a plataforma Banco Liquidante.


Exemplo de código completo

var crypto = require('crypto');

const privateKey = "NTRlNzM0NGMtNTdmMC00MjQ4LThiZTEtM2ZhMkDg4NzcwZTA5";
const publicKey = "MGE4NDIwM2ItNmU5Yi00Zjk0LWkE5NmEtNWIwMDdiOGVjMjJj";
const partnerUri = "https://url.com.br/api/webhooks";
const receivedHMAC = "wOeh+Yb1UoITqzQmvjafauh2kOp/w5kAEkqomsEhj8NQ=";

// Dados variáveis
const requestTimestamp = "1637839252";
const nonce = "ff4bb8520918k48f1a896d6f92d1e7605";
const requestBodyBase64 = "W3siZW50aXR5SWQiOi..."; // corpo da requisição em base64

const requestUriEncoded = encodeURIComponent(partnerUri).toLowerCase();
const separator = "&";
const preHashedString = `${publicKey}${separator}${requestUriEncoded}${separator}${requestTimestamp}${separator}${nonce}${separator}${requestBodyBase64}`;

const signature = crypto
  .createHmac('sha256', new Buffer.from(privateKey, 'base64').toString())
  .update(preHashedString, 'utf8')
  .digest('base64');

console.log(signature);

if (receivedHMAC === signature) {
  console.log("Chave HMAC válida");
}

Evitando um replay attack

Um replay attack (ataque de repetição) ocorre quando uma entidade maliciosa intercepta e reenvia uma transmissão de dados válida. Como os dados originais são legítimos, os protocolos de segurança da rede tratam o ataque como uma transmissão normal.

Os webhooks do Banco Liquidante disponibilizam dois campos que podem ser utilizados para mitigar esse tipo de ataque: nonce e requestTimestamp, ambos presentes no header da requisição.

Exemplo

Se o webhook do Banco Liquidante gerar o nonce "abcd1234" e enviá-lo no header, o servidor pode verificar se esse valor já foi utilizado anteriormente.

O servidor pode armazenar o nonce em cache por 5 minutos. Qualquer requisição recebida com o mesmo nonce dentro desse intervalo pode ser considerada um replay attack e rejeitada.

Após 5 minutos, o mesmo nonce pode ser aceito sem problema. Porém, como um agente malicioso poderia reenviar a requisição após esse intervalo, o campo requestTimestamp complementa a proteção: se o timestamp da requisição for mais antigo que 5 minutos em relação ao horário atual, a requisição pode ser rejeitada pelo servidor.

Como o requestTimestamp faz parte dos dados brutos da assinatura HMAC, qualquer tentativa de alterá-lo resultará em uma assinatura diferente, que não corresponderá à enviada pelo Banco Liquidante.

📘

Nota

O exemplo acima não é uma regra. A implementação da proteção contra replay attack fica a critério do parceiro.


Idempotency

Seguindo os princípios de design REST, a API do Banco Liquidante foi projetada para ser robusta em caso de falha.

Em transações financeiras, é fundamental que interrupções na comunicação não resultem em perda ou duplicação de dados. O sistema é projetado para que uma solicitação execute a ação desejada exatamente uma vez, independentemente de quantas vezes ela seja repetida. Esse conceito é chamado de Idempotência.

O Banco Liquidante oferece suporte a solicitações idempotentes por meio do header idempotency-Key: {{key}} incluído em cada requisição.

Responsabilidades do parceiro

O endpoint webhook do parceiro deve garantir a idempotência, armazenando a chave de idempotência enviada pelo sistema de webhooks do Banco Liquidante em cada solicitação.

A chave de idempotência deve estar no formato UUID (32 dígitos hexadecimais no padrão 8-4-4-4-12, por exemplo: 01234567-9abc-def0-1234-56789abcdef0) e ser única, para evitar conflitos ou duplicações.

📘

Nota

As chaves de idempotência devem ser armazenadas por um período mínimo de 7 dias.