Verificar firmas

La verificación de la firma de notificaciones te permite validar que cada notificación que hace Conekta es legítima

Es importante verificar la integridad de todas las notificaciones que Conekta te envía a tu 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.1.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

import java.security.*;
import java.security.spec.*;
import java.util.Base64;

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

        String 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}";

        String publicKeyPEM = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0iz57mpVAvxQtuxOyWsW\nhM1Jai7WB5cNZFs8xK53A9X9LQiXz30pzoFIhVo9Zm5K5GBpb9frCH8w6yr+/xrX\n0VUjbp1VTgZ2iGOm83ykLN7YYQJk5pCt/B69eFPYbMCKFzvauwTtN9tf2KcLQQ2y\nSohxd3H51uUIGcxnSR5oVPoCdY4geSWfK0/FE4SAyVsTB/b3mS0KUor7R2tZupKm\nrS26O6QFQrk0ELuGIIriJimjxaQG9V7E/TumKkbDPAcJsiZBF8oep02sXbdNpaxl\nj5PNkVIQ2F09BfDJl71DrcAIKYXG7HSgDEoiRkZ3jIzudUNA+qkpYwHJ5Qx9qmgy\nuQIDAQAB\n-----END PUBLIC KEY-----\n";

        PublicKey publicKey = getPublicKeyFromString(publicKeyPEM);


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

        Signature sig = Signature.getInstance("SHA256withRSA");
        sig.initVerify(publicKey);
        sig.update(payload.getBytes());
        boolean verified = sig.verify(Base64.getDecoder().decode(signature));

        if (verified) {
            System.out.println("Verification succeeded!");
        } else {
            System.out.println("Verification failed!");
        }
    }

    public static PublicKey getPublicKeyFromString(String key) throws Exception {
        String publicKeyPEM = key.replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replaceAll("\\s", "");

        byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyPEM);

        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
        return keyFactory.generatePublic(keySpec);
    }
}


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!")
}