Geração de assinatura HMAC

stable

Todos os eventos enviados pelos Webhooks Bankly possuem uma assinatura HMAC.

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

Para que nosso parceiro possa validar a mensagem recebida, ele também deverá gerar uma assinatura HMAC, que deve apresentar o mesmo algoritmo enviado pelo Bankly para que a autenticação ocorra.

Veja a seguir todas as etapas para a geração dessa assinatura.

❗️

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 Bankly.

Nessas chamadas HTTP, enviamos os seguintes headers necessários para que a aplicação cliente faça a geração da assinatura HMAC:

HeaderDescriçãoValor de exemplo
authorizationNeste campo, é enviado a assinatura HMAC.
Esse é o valor que deverá ser utilizado pela aplicação do cliente para validar se a mensagem recebida é válida ou não.
O valor é uma HMACSHA256 em base64.
hmac lBywDt0rKwAaJHz3cKxzFw6ptdQHrelLE7kbGAbLZ5A=
publicKeyNeste campo, é enviado a chave pública, que foi registrada na plataforma de webhooks do Bankly para o evento que está sendo enviado.
O valor é uma string em base64.
NWUyNjgwZDMtNmE2Ni00YWYzLWJkNjUtMGM2ODMzYzczYzI1
nonceNeste campo, é enviado um valor único e aleatório por requisição que a plataforma do Bankly fizer para a aplicação do cliente, o que aumenta a segurança do algoritmo.
O valor é uma UUID sem os hífens.
972004b06b6b443d8ed71630c9430048
requestTimestampNeste campo, é enviado o Unix Timestamp do momento em que a plataforma do Bankly efetuou requisição.1615331979
idempotency-KeyNeste campo, é enviado a chave de idempotência, que identifica a nossa 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. Ou seja, se o parceiro receber pela segunda vez a mesma chave, ele não deve processar a requisição.

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 efetuar a concatenação das informações recebidas nos headers citados acima. Os campos a seguir devem ser concatenados em uma única string:

  • publicKey: chave pública, a qual, recomendamos, que seja gerada uma por evento;
  • requestUri: a URI na qual a aplicação está recebendo a mensagem de webhook precisa estar codificada (percent-encode) e em lower case (em letras minúsculas);
  • requestTimestamp: data e horário em que a plataforma Bankly efetuou a requisição para a aplicação cliente;
  • nonce: valor aleatório que é gerado para cada requisição efetuada pela plataforma Bankly;
  • requestBodyBase64: corpo da requisição em formato raw (string não formatada) que a plataforma Bankly enviou. Esse payload deve ser convertido para base64 antes de ser concatenado.

🚧

Importante

Os campos devem seguir a ordem apresentada anteriormente e possuir o separador & entre cada valor.

Exemplo de concatenação dos dados

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

Com o valor dessa string já é possivel gerar a assinatura HMAC

Algoritmo de hash seguro

O algoritmo criptográfico para geração utilizado no Webhook Bankly é o SHA-256.

SHA-256 significa "algoritmo de hash seguro de 256 bits" e é usado para segurança criptográfica. Os algoritmos de hash criptográficos produzem hashes irreversíveis e exclusivos. Quanto maior for o número de hashes, menor será a chance de dois valores criarem o mesmo hash.

Exemplo de geração de assinatura HMAC

A seguir, exemplificamos a geração de uma assinatura HMAC a partir de uma chamada HTTP efetuada pela plataforma Bankly.

  1. Digamos que uma requisição HTTP POST seja feita para a seguinte URI:
    https://url.com.br/api/webhooks

  2. Levaremos em consideração que o Bankly enviará os seguintes valores nos headers previamente apresentados:

HeaderValor
requestUrihttps://url.com.br/api/webhooks
publicKeyNWUyNjgwZDMtNmE2Ni00YWYzLWJkNjUtMGM2ODMzYzczYzI1
requestTimeStamp1615331979
nonce972004b06b6b443d8ed71630c9430048
  1. No body da requisição, será enviado o payload a seguir:
'[  
     {     
          "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

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

A plataforma de Webhooks do Bankly vai efetuar uma requisição como esta:

--location --request POST 'https://pudim.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"}"  
    }  
]'
  1. Ao receber essa requisição, o parceiro deverá concatenar todos os valores em uma string, que será utilizada em seguida para gerar o hash HMAC.

String antes da geração do HMAC

const separator = "&"; const preHashedString = `${publicKey}{separator}${requestUri}{separator}${requestTimestamp}{separator}${nonce}{separator}${requestBodyBase64}` console.log(preHashedString) //NWUyNjgwZDMtNmE2Ni00YWYzLWJkNjUtMGM2ODMzYzczYzI1&https://url.com.br/api/webhooks&1615331979&972004b06b6b443d8ed71630c9430048&W3siZW50aXR5SWQiOiIzYzkyYjYyOS1mYzg5LTRkMDEtOTE0Ny00OGZjNWU3NGQ4ZDAiLCJjb21wYW55S2V5IjoiQUNvbXBhbnlLZXkiLCJuYW1lIjoidHJhbnNhY3Rpb24uaG9sZC53YXMuYXBwcm92ZWQiLCJ0aW1lc3RhbXAiOiIyMDIxLTAzLTA5VDIzOjIyOjAwIiwiY29ycmVsYXRpb25JZCI6IjE4OGJlNzA4LWZmNDAtNDhhZi1iOWMxLTFlZDllMzM1YWQ5OSIsIm1ldGFkYXRhIjp7IkZvbyI6IkJhciJ9LCJkYXRhIjoie1wiQmFyXCI6XCIzMVwifSJ9XQ==
  1. Depois de ter concatenado todos os valores, o parceiro irá gerar a assinatura HMAC em base64. Para isso, será preciso preencher dois campos:
  • privateKey: chave privada de forma idêntica à informada na configuração do webhook. Ou seja, ela não deve ser passada em base64.
  • preHashedString: string demonstrada no item anterior, em formato UTF8.

Exemplo de geração da assinatura HMAC

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

🚧

Importante

A Chave privada (privateKey) nunca é enviada na requisição por parte do Bankly. Essa chave deve ser armazenada pelo parceiro de forma segura e utilizada somente no momento da geração do hash HMAC.

  1. Para que a assinatura HMAC gerada pelo parceiro seja validada, ela deve ser idêntica à assinatura do header authorization enviada pelo Bankly.

❗️

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 Bankly.

Exemplo de código

Veja a seguir um exemplo de código com todos os passos descritos anteriormente. Note que ele está no formato UTF8:

var crypto = require('crypto');
const privateKey = "NTRlNzM0NGMtNTdmMC00MjQ4LThiZTEtM2ZhMkDg4NzcwZTA5";
const publicKey = "MGE4NDIwM2ItNmU5Yi00Zjk0LWkE5NmEtNWIwMDdiOGVjMjJj";
const partnerUri = "https://6754ad618kb443edafef4d9af5fcff304.m.pipedream.net";
const receivedHMAC = "wOeh+Yb1UoITqzQmvjafauh2kOp/w5kAEkqomsEhj8NQ="

//Dados variáveis
const requestTimestamp = "1637839252";
const nonce = "ff4bb8520918k48f1a896d6f92d1e7605";

const requestBodyBase64= "W3siZW50aXR5SWQiOiIzM2MyOTAxYy05YzVmLTQxYTctOGVjMC1lZjM2NTkwZWU2NzEiLCJjb21wYW55S2V5IjoiSU5URVJOT19VUFNJR0hUIiwiaWRlbXBvdGVuY3lLZXkiOiJiOTJmZWFiMy0yMDNiLTQyMzEtOGY3NS1jNzhjNjlkMDMyYjciLCJjb250ZXh0IjoiQm9sZXRvIiwibmFtZSI6IkJPTEVUT19DQVNIX0lOX1dBU19DTEVBUkVEIiwidGltZXN0YW1wIjoiMjAyMS0xMS0yNVQxMToyMDo1MS41ODc5MTYzWiIsImNvcnJlbGF0aW9uSWQiOiIyYzUwMjZkMS0wMDllLTlhZDktYzIzNS1mYWRkOTc2NDBiMjkiLCJtZXRhZGF0YSI6bnVsbCwiZGF0YSI6eyJhdXRoZW50aWNhdGlvbkNvZGUiOiIzM2MyOTAxYy05YzVmLTQxYTctOGVjMC1lZjM2NTkwZWU2NzEiLCJhbW91bnQiOnsidmFsdWUiOjEuMCwiY3VycmVuY3kiOiJCUkwifSwicmVjaXBpZW50Ijp7ImRvY3VtZW50Ijp7InZhbHVlIjoiNDMwNTA5MDI4NzYiLCJ0eXBlIjoiQ1BGIn0sInR5cGUiOiJDdXN0b21lciIsIm5hbWUiOiJWaWN0b3IgUGFwaSBSYW1vcyIsImFjY291bnQiOnsiYnJhbmNoIjoiMDAwMSIsIm51bWJlciI6IjExMTk5MTEzIiwiYmFuayI6eyJpc3BiIjoiMTMxNDAwODgiLCJjb2RlIjoiMzMyIiwibmFtZSI6IkFjZXNzbyBTb2x1w6fDtWVzIGRlIFBhZ2FtZW50byBTLkEuIn19fSwiY2hhbm5lbCI6eyJuYW1lIjoiQm9sZXRvIiwib3VyTnVtYmVyIjoiMzQ2MTgyOTkzNzUifSwiY3JlYXRlZEF0IjoiMjAyMS0xMS0yNVQxMToyMDo0OS40Nzk1NjE0WiJ9fV0="

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) é um ataque no qual uma entidade maliciosa intercepta e repete uma transmissão de dados válida que trafega por uma rede. Devido à validade dos dados originais (que normalmente vêm de um usuário autorizado), os protocolos de segurança da rede tratam o ataque como se fosse uma transmissão de dados normal, já que as mensagens originais são interceptadas e retransmitidas textualmente.

A solução de Webhook Bankly disponibiliza ao menos dois campos que podem ser utilizados para que seja possível ao parceiro evitar replay attack: os campos nonce e requestTimestamp, ambos presentes no header da requisição.

Vamos entender como evitar o replay attack com o exemplo a seguir.

Exemplo

Se Webhook Bankly gerar um nonce “abcd1234” e enviá-lo no header da requisição, o servidor pode verificar se esse nonce “abcd1234” foi usado anteriormente.

Então, o servidor pode armazenar o valor do nonce em cache pelos próximos 5 minutos. Dessa forma, qualquer solicitação proveniente do Bankly com o mesmo valor de nonce, ou seja, "abcd1234", durante o intervalo de tempo de 5 minutos pode ser considerada como um replay attack.

Se o mesmo nonce “abcd1234” for usado após um intervalo de tempo de 5 minutos, não há problema, e a solicitação pode não ser considerada como uma solicitação de repetição.

Porém, uma pessoa maliciosa pode postar novamente a mesma solicitação usando o mesmo nonce após o intervalo de 5 minutos. Nesses casos, o header requestTimestamp que contém a data e a hora da solicitação torna-se útil, pois o servidor pode comparar a data e a hora UNIX atual com a data e a hora UNIX da requisição. Se o requestTimestamp for mais antigo do que 5 minutos, então a requisição pode ser rejeitada pelo servidor, e a pessoa maliciosa não terá possibilidade de falsificar/alterar o requestTimestamp enviando um mais recente, porque já o incluímos nos dados brutos da assinatura HMAC.

Portanto, qualquer alteração nos dados assinados resultará em uma nova assinatura e não corresponderá à assinatura do Bankly.

📘

Nota

O exemplo anterior não é regra e, apesar de poder ser utilizado como estratégia para evitar o replay attack, fica a critério do parceiro implementar a segurança contra o esse tipo de ataque.

Idempotency

Seguindo os princípios de design REST, nossa API foi projetada para ser robusta em caso de falha.

Ao trabalhar com transações financeiras, é especialmente importante que as interrupções na comunicação não resultem em perda acidental ou duplicação de dados.

Se um usuário enviar uma solicitação, mas nunca receber uma resposta, ele deve ser capaz de repetir a solicitação com segurança, sem medo de realizar a ação acidentalmente duas vezes.

Em outras palavras, o sistema deve ser projetado de forma que uma solicitação execute a ação desejada exatamente uma vez, independentemente da frequência com que ela seja repetida. Esse conceito é denominado Idempotência.

O Bankly oferece suporte a solicitações idempotentes por meio do cabeçalho idempotency-Key: {{key}} incluído em cada solicitação.

Responsabilidades do cliente

O endpoint webhook do parceiro deve garantir a idempotência e deve armazenar a chave de idempotência que nosso sistema de webhook envia em cada solicitação.

A chave de idempotência deve ter o formato UUID (32 dígitos hexadecimais, em um arranjo 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.