Autenticación Webhooks

Es importante verificar la integridad de todas las notificaciones que Conekta te envía por webhook para asegurar que son legítimos y que no han sido modificados. El mecanismo para hacer esta verificación involucra un juego de llaves RSA público/privado para tu compañía para firmar un sha256 del cuerpo de la petición.

Inicialización - Solo Una Vez

El primer paso es crear un juego de llaves RSA en Conekta, vas a recibir la llave pública para verificar mensajes y Conekta se va a quedar con la llave privada para firmar mensajes. Solo es necesario realizar este proceso una vez. Puedes iniciar llaves de webhook con un curl así:

curl --request POST \
  --url https://api.conekta.io/webhook_keys \
  --header 'accept: application/vnd.conekta-v2.0.0+json' \
  -u key_eYvWV7gSDkNYXsmr: \
  --header 'content-type: application/json' \
  --data-raw '{
    "active": true
  }'

Lo cuál va retornar el siguiente mensaje

{
  "active": true,
  "livemode": false,
  "created_at": 1651706790,
  "id": "62730ba6fb7dfd6a712f118e",
  "object": "webhook_key",
  "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0iz57mpVAvxQtuxOyWsW\nhM1Jai7WB5cNZFs8xK53A9X9LQiXz30pzoFIhVo9Zm5K5GBpb9frCH8w6yr+/xrX\n0VUjbp1VTgZ2iGOm83ykLN7YYQJk5pCt/B69eFPYbMCKFzvauwTtN9tf2KcLQQ2y\nSohxd3H51uUIGcxnSR5oVPoCdY4geSWfK0/FE4SAyVsTB/b3mS0KUor7R2tZupKm\nrS26O6QFQrk0ELuGIIriJimjxaQG9V7E/TumKkbDPAcJsiZBF8oep02sXbdNpaxl\nj5PNkVIQ2F09BfDJl71DrcAIKYXG7HSgDEoiRkZ3jIzudUNA+qkpYwHJ5Qx9qmgy\nuQIDAQAB\n-----END PUBLIC KEY-----\n"
}

Puedes ver más detalle en la sección de en la referencia de llaves para webhooks.

Verificación de Mensajes - En Cada Mensaje

Puedes validar la integridad del mensaje consumiendo el header de 'DIGEST' y comprobando que representa el sha256 del mensaje cifrado por la llave privada. Como nota, es esperado que la codificación del payload está en UTF-8.

{"data":{"object":{"id":"61fdc53b0211a6764e57ec4f","livemode":true,"created_at":1644021051,"currency":"MXN","channel":{"segment":"Checkout","checkout_request_id":"4ed49de7-60e7-4e87-a0de-48b433f3ea2f","checkout_request_type":"PaymentLink","id":"channel_2rFeDewRPQpn3NFoe"},"payment_method":{"name":"OscarNava","exp_month":"12","exp_year":"22","object":"card_payment","type":"credit","last4":"0002","brand":"visa","issuer":"banamex","account_type":"BANAMEX","country":"MX","fraud_indicators":[]},"object":"charge","description":"Payment from order","status":"declined","amount":10000,"fee":708,"customer_id":"cus_2rFeDEaPCq2GkhLS8","order_id":"ord_2rFeDeCbsDG8R6bK4"},"previous_attributes":{}},"livemode":true,"webhook_status":"failing","webhook_logs":[{"id":"webhl_2rFeDewRPQpn3NFoj","url":"https://www.example.com/conekta/webhooks","failed_attempts":5,"last_http_response_status":598,"object":"webhook_log","last_attempted_at":0}],"id":"61fdc53b0211a6764e57ec53","object":"event","type":"charge.created","created_at":1644021051}

Digest ejemplo:

PY0lEriF5Tt4T0ZVqwEQtS5+skbqUpCWwwDs5EtBEX5vriK15KuLCuTndttDR4jvlSvDsDVSUOCxXrpxMJ0x0cePCa79xGa23r/hcv4CPcfr2UMe4IKJuJSlJc3XCfOQB1rfk6fQCMr7AbiVtvacr91yYxg7QoJq2/Y0YRo7RreT61X7/dmGhgzFrG0TALQ2R0PbQyAXIvO7l5+00Yncdc0IeyCBTL3/Wa0zN+Dc4UZW3iWFpNOIyljDKxmBp+2D0DxPeLfXw7fnVskQMSCM7rCVXSoP7k8BCyIDRZ62QieMXW5CubixFaPFrI1f6K8gh67lguYF2XSY/bYb7IU4Jg==

Puedes comprobar la integridad del mensaje con el siguiente código

#decode base64
cat DIGEST.sha256.base64 | base64 --decode > signature.sha256

#Verify digest
openssl dgst -sha256 -verify pubkey.pem -signature signature.sha256 notification_payload.utf8.json

#Verified OK
require 'openssl'
require 'base64'
pubkey = OpenSSL::PKey::RSA.new("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0iz57mpVAvxQtuxOyWsW\nhM1Jai7WB5cNZFs8xK53A9X9LQiXz30pzoFIhVo9Zm5K5GBpb9frCH8w6yr+/xrX\n0VUjbp1VTgZ2iGOm83ykLN7YYQJk5pCt/B69eFPYbMCKFzvauwTtN9tf2KcLQQ2y\nSohxd3H51uUIGcxnSR5oVPoCdY4geSWfK0/FE4SAyVsTB/b3mS0KUor7R2tZupKm\nrS26O6QFQrk0ELuGIIriJimjxaQG9V7E/TumKkbDPAcJsiZBF8oep02sXbdNpaxl\nj5PNkVIQ2F09BfDJl71DrcAIKYXG7HSgDEoiRkZ3jIzudUNA+qkpYwHJ5Qx9qmgy\nuQIDAQAB\n-----END PUBLIC KEY-----\n")

payload = '{"data":{"object":{"id":"61fdc53b0211a6764e57ec4f","livemode":true,"created_at":1644021051,"currency":"MXN","channel":{"segment":"Checkout","checkout_request_id":"4ed49de7-60e7-4e87-a0de-48b433f3ea2f","checkout_request_type":"PaymentLink","id":"channel_2rFeDewRPQpn3NFoe"},"payment_method":{"name":"OscarNava","exp_month":"12","exp_year":"22","object":"card_payment","type":"credit","last4":"0002","brand":"visa","issuer":"banamex","account_type":"BANAMEX","country":"MX","fraud_indicators":[]},"object":"charge","description":"Payment from order","status":"declined","amount":10000,"fee":708,"customer_id":"cus_2rFeDEaPCq2GkhLS8","order_id":"ord_2rFeDeCbsDG8R6bK4"},"previous_attributes":{}},"livemode":true,"webhook_status":"failing","webhook_logs":[{"id":"webhl_2rFeDewRPQpn3NFoj","url":"https://www.example.com/conekta/webhooks","failed_attempts":5,"last_http_response_status":598,"object":"webhook_log","last_attempted_at":0}],"id":"61fdc53b0211a6764e57ec53","object":"event","type":"charge.created","created_at":1644021051}'

signature = 'PY0lEriF5Tt4T0ZVqwEQtS5+skbqUpCWwwDs5EtBEX5vriK15KuLCuTndttDR4jvlSvDsDVSUOCxXrpxMJ0x0cePCa79xGa23r/hcv4CPcfr2UMe4IKJuJSlJc3XCfOQB1rfk6fQCMr7AbiVtvacr91yYxg7QoJq2/Y0YRo7RreT61X7/dmGhgzFrG0TALQ2R0PbQyAXIvO7l5+00Yncdc0IeyCBTL3/Wa0zN+Dc4UZW3iWFpNOIyljDKxmBp+2D0DxPeLfXw7fnVskQMSCM7rCVXSoP7k8BCyIDRZ62QieMXW5CubixFaPFrI1f6K8gh67lguYF2XSY/bYb7IU4Jg=='

if pubkey.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), payload)
  puts "Verification succeeded!"
end
package org.conekta;

import java.nio.charset.StandardCharsets;

import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class Main {
    public static void main(String[] args) throws Exception {

        String message = "{\"data\":{\"object\":{\"id\":\"61fdc53b0211a6764e57ec4f\",\"livemode\":true,\"created_at\":1644021051,\"currency\":\"MXN\",\"channel\":{\"segment\":\"Checkout\",\"checkout_request_id\":\"4ed49de7-60e7-4e87-a0de-48b433f3ea2f\",\"checkout_request_type\":\"PaymentLink\",\"id\":\"channel_2rFeDewRPQpn3NFoe\"},\"payment_method\":{\"name\":\"OscarNava\",\"exp_month\":\"12\",\"exp_year\":\"22\",\"object\":\"card_payment\",\"type\":\"credit\",\"last4\":\"0002\",\"brand\":\"visa\",\"issuer\":\"banamex\",\"account_type\":\"BANAMEX\",\"country\":\"MX\",\"fraud_indicators\":[]},\"object\":\"charge\",\"description\":\"Payment from order\",\"status\":\"declined\",\"amount\":10000,\"fee\":708,\"customer_id\":\"cus_2rFeDEaPCq2GkhLS8\",\"order_id\":\"ord_2rFeDeCbsDG8R6bK4\"},\"previous_attributes\":{}},\"livemode\":true,\"webhook_status\":\"failing\",\"webhook_logs\":[{\"id\":\"webhl_2rFeDewRPQpn3NFoj\",\"url\":\"https://www.example.com/conekta/webhooks\",\"failed_attempts\":5,\"last_http_response_status\":598,\"object\":\"webhook_log\",\"last_attempted_at\":0}],\"id\":\"61fdc53b0211a6764e57ec53\",\"object\":\"event\",\"type\":\"charge.created\",\"created_at\":1644021051}";
        byte[] signature = "PY0lEriF5Tt4T0ZVqwEQtS5+skbqUpCWwwDs5EtBEX5vriK15KuLCuTndttDR4jvlSvDsDVSUOCxXrpxMJ0x0cePCa79xGa23r/hcv4CPcfr2UMe4IKJuJSlJc3XCfOQB1rfk6fQCMr7AbiVtvacr91yYxg7QoJq2/Y0YRo7RreT61X7/dmGhgzFrG0TALQ2R0PbQyAXIvO7l5+00Yncdc0IeyCBTL3/Wa0zN+Dc4UZW3iWFpNOIyljDKxmBp+2D0DxPeLfXw7fnVskQMSCM7rCVXSoP7k8BCyIDRZ62QieMXW5CubixFaPFrI1f6K8gh67lguYF2XSY/bYb7IU4Jg==".getBytes();

        byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
        String bytekey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0iz57mpVAvxQtuxOyWsW\nhM1Jai7WB5cNZFs8xK53A9X9LQiXz30pzoFIhVo9Zm5K5GBpb9frCH8w6yr+/xrX\n0VUjbp1VTgZ2iGOm83ykLN7YYQJk5pCt/B69eFPYbMCKFzvauwTtN9tf2KcLQQ2y\nSohxd3H51uUIGcxnSR5oVPoCdY4geSWfK0/FE4SAyVsTB/b3mS0KUor7R2tZupKm\nrS26O6QFQrk0ELuGIIriJimjxaQG9V7E/TumKkbDPAcJsiZBF8oep02sXbdNpaxl\nj5PNkVIQ2F09BfDJl71DrcAIKYXG7HSgDEoiRkZ3jIzudUNA+qkpYwHJ5Qx9qmgy\nuQIDAQAB\n-----END PUBLIC KEY-----\n";
        PublicKey key = load(bytekey);


        Signature sig = Signature.getInstance("SHA256WithRSA");
        sig.initVerify(key);
        sig.update(messageBytes);
        boolean result = sig.verify(Base64.getDecoder().decode(signature));

        // result
        System.out.println("Message   = " + message);
        System.out.println("Signature = "
                + Base64.getDecoder().decode(signature));
        System.out.println("Verification Result = " + result);
    }

    public static PublicKey load(String publicKeyContent) throws Exception {
        publicKeyContent = publicKeyContent.
                replaceAll("\\n", "").
                replace("-----BEGIN PUBLIC KEY-----", "").
                replace("-----END PUBLIC KEY-----", "");
        ;

        X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyContent));
        KeyFactory kf = KeyFactory.getInstance("RSA");

        return kf.generatePublic(X509publicKey);
    }
}
const NodeRSA = require('node-rsa');

const publicKey = new NodeRSA("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0iz57mpVAvxQtuxOyWsW\nhM1Jai7WB5cNZFs8xK53A9X9LQiXz30pzoFIhVo9Zm5K5GBpb9frCH8w6yr+/xrX\n0VUjbp1VTgZ2iGOm83ykLN7YYQJk5pCt/B69eFPYbMCKFzvauwTtN9tf2KcLQQ2y\nSohxd3H51uUIGcxnSR5oVPoCdY4geSWfK0/FE4SAyVsTB/b3mS0KUor7R2tZupKm\nrS26O6QFQrk0ELuGIIriJimjxaQG9V7E/TumKkbDPAcJsiZBF8oep02sXbdNpaxl\nj5PNkVIQ2F09BfDJl71DrcAIKYXG7HSgDEoiRkZ3jIzudUNA+qkpYwHJ5Qx9qmgy\nuQIDAQAB\n-----END PUBLIC KEY-----\n");
const signature = 'PY0lEriF5Tt4T0ZVqwEQtS5+skbqUpCWwwDs5EtBEX5vriK15KuLCuTndttDR4jvlSvDsDVSUOCxXrpxMJ0x0cePCa79xGa23r/hcv4CPcfr2UMe4IKJuJSlJc3XCfOQB1rfk6fQCMr7AbiVtvacr91yYxg7QoJq2/Y0YRo7RreT61X7/dmGhgzFrG0TALQ2R0PbQyAXIvO7l5+00Yncdc0IeyCBTL3/Wa0zN+Dc4UZW3iWFpNOIyljDKxmBp+2D0DxPeLfXw7fnVskQMSCM7rCVXSoP7k8BCyIDRZ62QieMXW5CubixFaPFrI1f6K8gh67lguYF2XSY/bYb7IU4Jg=='
const payload = '{"data":{"object":{"id":"61fdc53b0211a6764e57ec4f","livemode":true,"created_at":1644021051,"currency":"MXN","channel":{"segment":"Checkout","checkout_request_id":"4ed49de7-60e7-4e87-a0de-48b433f3ea2f","checkout_request_type":"PaymentLink","id":"channel_2rFeDewRPQpn3NFoe"},"payment_method":{"name":"OscarNava","exp_month":"12","exp_year":"22","object":"card_payment","type":"credit","last4":"0002","brand":"visa","issuer":"banamex","account_type":"BANAMEX","country":"MX","fraud_indicators":[]},"object":"charge","description":"Payment from order","status":"declined","amount":10000,"fee":708,"customer_id":"cus_2rFeDEaPCq2GkhLS8","order_id":"ord_2rFeDeCbsDG8R6bK4"},"previous_attributes":{}},"livemode":true,"webhook_status":"failing","webhook_logs":[{"id":"webhl_2rFeDewRPQpn3NFoj","url":"https://www.example.com/conekta/webhooks","failed_attempts":5,"last_http_response_status":598,"object":"webhook_log","last_attempted_at":0}],"id":"61fdc53b0211a6764e57ec53","object":"event","type":"charge.created","created_at":1644021051}'

const isValid = publicKey.verify(payload, signature, 'utf8', 'base64');

if (isValid) {
  console.log("Verification succeeded!")
}