Verificar firmas
La verificación de la firma de notificaciones te permite validar que cada notificación que hace Oxxo Pay es legítima
Es importante verificar la integridad de todas las notificaciones que Oxxo Pay 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 Oxxo Pay, vas a recibir la llave pública para verificar mensajes y Oxxo Pay 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.digitalfemsa.io/webhook_keys \
--header 'accept: application/vnd.app-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":{"livemode":true,"action":"webhook_ping"},"livemode":true,"webhook_status":"failing","webhook_logs":[{"id":"webhl_2vz9xwcbRZMrLmLv7","url":"https://5a76-190-31-151-164.ngrok-free.app/internal_femsa/webhooks","failed_attempts":1,"last_http_response_status":598,"response_data":null,"object":"webhook_log","last_attempted_at":0}],"id":"6650ca05a0badd0001dac565","object":"event","type":"webhook_ping","created_at":1716570629}
Digest ejemplo:
xrcrGsFdF1UYhdMQlbegyPFdKEoFWKTKMVAnvEs+Ue1TVUyTDYKXGLWv4qlgXCRpJI4LaeEqJ2GXOunjVMbkfLn5r8Ekro/mUQr6E8RZE9o0Pf6yo9drGO+/HjkVxwcLh0BzLtX75k5A4daoemHg0jXkNsHsknNkD0wjfrVlsc0PvnaEfT1R+zsoLpSIsVD6zWxxBfZkKqwMI2zSb2olpaAVsfNGtMiE9BNF3miaShjEs8AXKneUC2wcDIqndrrSSH6gl0xhJb1QZhTBnxHuVVCK5BcRdiRDyWZkUKzkIPD3E4OKBFLROqtp/YefC92v1LfSZNuYmWzxZmr09SgJow==
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-----\n MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4v+Q2BBx1kKFo3k/S1A7 bJVGxz6q5Ld+4IxuDPOAzEUkeaLOKX+Q946OyTE/VVtWWHcF0Qe2fIJhXqarr4fI Ah+wAAk7tAQPWiDlhf+zHdSzHSWgJOHM5IHWNWbzY9juOC/LEQd+eEWRJTk2APX2 hm03OkFPUHkhFf+/oZ4W1IlGOPnK/KU9xuf3Y/8A6MGGWdx2KOFKvEpf7kuVGoNF TzOjNNp1GeSoGCP9InZiWx7NkFPQoK+9cdEofNtJ8Hyt0A8/PWfS88J3mbenzrl9 3dIcra8lUi+VSATdO42U0tqMcdwdpFok71ejY+QtWCr9PyMjwy6+zSE4F6kcUhPd lwIDAQAB\n -----END PUBLIC KEY-----")
payload = '{"data":{"livemode":true,"action":"webhook_ping"},"livemode":true,"webhook_status":"failing","webhook_logs":[{"id":"webhl_2vz9xwcbRZMrLmLv7","url":"https://5a76-190-31-151-164.ngrok-free.app/internal_femsa/webhooks","failed_attempts":1,"last_http_response_status":598,"response_data":null,"object":"webhook_log","last_attempted_at":0}],"id":"6650ca05a0badd0001dac565","object":"event","type":"webhook_ping","created_at":1716570629}'
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\":{\"livemode\":true,\"action\":\"webhook_ping\"},\"livemode\":true,\"webhook_status\":\"failing\",\"webhook_logs\":[{\"id\":\"webhl_2vz9xwcbRZMrLmLv7\",\"url\":\"https://5a76-190-31-151-164.ngrok-free.app/internal_femsa/webhooks\",\"failed_attempts\":1,\"last_http_response_status\":598,\"response_data\":null,\"object\":\"webhook_log\",\"last_attempted_at\":0}],\"id\":\"6650ca05a0badd0001dac565\",\"object\":\"event\",\"type\":\"webhook_ping\",\"created_at\":1716570629}"";
String publicKeyPEM = "-----BEGIN PUBLIC KEY-----\n MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4v+Q2BBx1kKFo3k/S1A7 bJVGxz6q5Ld+4IxuDPOAzEUkeaLOKX+Q946OyTE/VVtWWHcF0Qe2fIJhXqarr4fI Ah+wAAk7tAQPWiDlhf+zHdSzHSWgJOHM5IHWNWbzY9juOC/LEQd+eEWRJTk2APX2 hm03OkFPUHkhFf+/oZ4W1IlGOPnK/KU9xuf3Y/8A6MGGWdx2KOFKvEpf7kuVGoNF TzOjNNp1GeSoGCP9InZiWx7NkFPQoK+9cdEofNtJ8Hyt0A8/PWfS88J3mbenzrl9 3dIcra8lUi+VSATdO42U0tqMcdwdpFok71ejY+QtWCr9PyMjwy6+zSE4F6kcUhPd lwIDAQAB\n -----END PUBLIC KEY-----";
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-----\n MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4v+Q2BBx1kKFo3k/S1A7 bJVGxz6q5Ld+4IxuDPOAzEUkeaLOKX+Q946OyTE/VVtWWHcF0Qe2fIJhXqarr4fI Ah+wAAk7tAQPWiDlhf+zHdSzHSWgJOHM5IHWNWbzY9juOC/LEQd+eEWRJTk2APX2 hm03OkFPUHkhFf+/oZ4W1IlGOPnK/KU9xuf3Y/8A6MGGWdx2KOFKvEpf7kuVGoNF TzOjNNp1GeSoGCP9InZiWx7NkFPQoK+9cdEofNtJ8Hyt0A8/PWfS88J3mbenzrl9 3dIcra8lUi+VSATdO42U0tqMcdwdpFok71ejY+QtWCr9PyMjwy6+zSE4F6kcUhPd lwIDAQAB\n -----END PUBLIC KEY-----");
const signature = 'PY0lEriF5Tt4T0ZVqwEQtS5+skbqUpCWwwDs5EtBEX5vriK15KuLCuTndttDR4jvlSvDsDVSUOCxXrpxMJ0x0cePCa79xGa23r/hcv4CPcfr2UMe4IKJuJSlJc3XCfOQB1rfk6fQCMr7AbiVtvacr91yYxg7QoJq2/Y0YRo7RreT61X7/dmGhgzFrG0TALQ2R0PbQyAXIvO7l5+00Yncdc0IeyCBTL3/Wa0zN+Dc4UZW3iWFpNOIyljDKxmBp+2D0DxPeLfXw7fnVskQMSCM7rCVXSoP7k8BCyIDRZ62QieMXW5CubixFaPFrI1f6K8gh67lguYF2XSY/bYb7IU4Jg=='
const payload = '{"data":{"livemode":true,"action":"webhook_ping"},"livemode":true,"webhook_status":"failing","webhook_logs":[{"id":"webhl_2vz9xwcbRZMrLmLv7","url":"https://5a76-190-31-151-164.ngrok-free.app/internal_femsa/webhooks","failed_attempts":1,"last_http_response_status":598,"response_data":null,"object":"webhook_log","last_attempted_at":0}],"id":"6650ca05a0badd0001dac565","object":"event","type":"webhook_ping","created_at":1716570629} '
const isValid = publicKey.verify(payload, signature, 'utf8', 'base64');
if (isValid) {
console.log("Verification succeeded!")
}
Updated 8 months ago