in Technical minutes

Webpay communication protocol

This is going to be a bit long and boring, but I promised to write about this so here it comes:

The base

The protocol can be divided in a few basic steps:

  1. The commerce generates a payment token by delivering all the payment information (amount, order id, redirection urls among others) to Webpay's servers through an application/x-www-form-urlencoded HTTP POST request with an encrypted TBK_PARAM attribute.
  2. Webpay's servers responds the request with an encrypted payload that contains an XML document with a TOKEN value.
  3. The user is redirected to Webpay payment flow using the token acquired by the commerce on the second step.
  4. Once the user completes the payment the commerce receives an application/x-www-form-urlencoded HTTP POST request with an encrypted TBK_PARAM attribute directly from Webpay's servers with the payment information (success or failure, amount, authorization code among others).
  5. The commerce responds to Webpay's request with an encrypted acknowledge message
  6. Finally the user is redirected back to the commerce website

Now given the communication flow outlined above there are 2 things to analyze.

  1. The encryption and decryption algorithms.
  2. The 4 messages exchanged between the commerce and Webpay:
    1. Payment details message (from step 1)
    2. Webpay token response (from step 2)
    3. Webpay confirmation details (from step 4)
    4. Commerce acknowledge message (from step 5)

The encryption and decryption algorithms.

This algorithm is based on a pair of asymmetric RSA keys exchanged between the commerce and Transbank.

In order to explain this algorithm I'm going to use some bytes of ruby code. So given a message, the private key of the sender (sender_key) and the public key of the recipient (recipient_key) we will do the following:

# Sign the message using the sender_key with a SHA512 Hash
signature = sender_key.sign(OpenSSL::Digest::SHA512.new, message)

# Generate a random symmetric key of 32 bytes
key = SecureRandom.random_bytes(32)
# Generate a random initialization vector of 16 bytes
iv = SecureRandom.random_bytes(16)

# Encrypt the message and signature with an AES 256 CBC cypher
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
# Use the generated key
cipher.key = key
# Use the generated initialization vector with a constant padding
cipher.iv = iv + "\x10\xBB\xFF\xBF\x00\x00\x00\x00\x00\x00\x00\x00\xF4\xBF"

# Encrypt the signature concatenated with the message
encrypted_message = cipher.update(signature + text) + cipher.final

# Encrypt the generated key with the recipient_key using PKCS1 OAEP padding
encrypted_key = recipient_key.public_encrypt(key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)

# And finally Base64 encode the initialization vector, the encrypted key and
# the encrypted message.
return Base64.encode64( iv + encrypted_key + encrypted_message)

(Check TBK::Webpay::Encryption#webpay_encrypt for a working example.)

And as should be expected the decryption algorithm is basically performing the same actions from the encryption algorithm in reverse order

data = Base64.decode64(encripted_message)

# Initialization vector is on the first 16 bytes
iv = data[0...16]

# the encrypted key comes next
encrypted_key = data[16...(16 + recipient_key_bytes)]

# Decrypt with the recipient_key using PKCS1 OAEP padding
key = recipient_key.private_decrypt(encrypted_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)

# Decrypt with an AES 256 CBC cypher
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.decrypt
cipher.key = key
cipher.iv = iv + "\x10\xBB\xFF\xBF\x00\x00\x00\x00\x00\x00\x00\x00\xF4\xBF"
decrypted_text = cipher.update(data[(16 + recipient_key_bytes)..-1]) + cipher.final

# Split signature and message
signature = decrypted_text[0...(sender_key_bytes)]
message = decrypted_text[(sender_key_bytes)..-1]

# Verify the message signature
if sender_key.verify(OpenSSL::Digest::SHA512.new, signature, message)
  return message
else
  raise "Invalid message signature"
end

(Check TBK::Webpay::Encryption#webpay_decrypt for a working example)

The messages exchanged between the commerce and Webpay

Now that we know how everything is encrypted all we need to know is what messages get exchanged.

First message

The first message consists on all the payment information and looks like:

TBK_ORDEN_COMPRA=d5eeb3e9a6ba51833cc3845ce70f6cbf#TBK_CODIGO_COMERCIO=597029614741#TBK_ID_TRANSACCION=2175685762#TBK_URL_CGI_COMERCIO=/notify#TBK_SERVIDOR_COMERCIO=127.0.0.1#TBK_PUERTO_COMERCIO=80#TBK_VERSION_KCC=6.0#TBK_KEY_ID=101#PARAMVERIFCOM=1#TBK_MAC=068b049f169272e5b8f3afcadaf928e5#TBK_MONTO=10000#TBK_URL_EXITO=http://asdf.com/#TBK_URL_FRACASO=http://asdf.com/#TBK_TIPO_TRANSACCION=TR_NORMAL

Where the key attributes are:

  • TBK_ORDEN_COMPRA: The commerce order id, an internal value that you can use to match a payment with your internal system.
  • TBK_CODIGO_COMERCIO: The commerce ID supplied by Transbank.
  • TBK_ID_TRANSACCION: A random number generated by the app.
  • TBK_URL_CGI_COMERCIO: The path that will receive the confirmation HTTP POST request.
  • TBK_SERVIDOR_COMERCIO: The server IP address that will receive the confirmation HTTP POST request.
  • TBK_PUERTO_COMERCIO: The server port that will receive the confirmation HTTP POST request.
  • TBK_MONTO: The amount in CLP "cents" (multiplied by 100).
  • TBK_URL_EXITO: The location where the user will be redirected on success.
  • TBK_URL_FRACASO: The location where the user will be redirected on failure.
  • TBK_MAC: A legacy signature scheme from an older and insecure version of the protocol but given the adobe message would be calculated like:
  md5 -s "TBK_ORDEN_COMPRA=d5eeb3e9a6ba51833cc3845ce70f6cbf&TBK_CODIGO_COMERCIO=597029614741&TBK_ID_TRANSACCION=2175685762&TBK_URL_CGI_COMERCIO=/notify&TBK_SERVIDOR_COMERCIO=127.0.0.1&TBK_PUERTO_COMERCIO=80&TBK_VERSION_KCC=6.0&TBK_KEY_ID=101&PARAMVERIFCOM=1&TBK_MONTO=10000&TBK_URL_EXITO=http://asdf.com/&TBK_URL_FRACASO=http://asdf.com/&TBK_TIPO_TRANSACCION=TR_NORMAL597029614741webpay"

(Check TBK::Webpay::Payment#raw_params for a working example) This message is encrypted and delivered via HTTP POST to the endpoint https://webpay.transbank.cl:443/cgi-bin/bp_validacion.cgi

Second message

This endpoint responds with an encrypted message that looks like:

ERROR=0
TOKEN=bffa44af9b61ca229e68e93607d81f91a2d3e4016ee227b3e629

Once you have the token all you have to do is redirect the user to the url:

https://webpay.transbank.cl:443/cgi-bin/bp_revision.cgi?TBK_VERSION_KCC=6.0&TBK_TOKEN=bffa44af9b61ca229e68e93607d81f91a2d3e4016ee227b3e629

Third message

Once the user has completed the payment your server will receive an encrypted message like:

TBK_ORDEN_COMPRA=3244#TBK_TIPO_TRANSACCION=TR_NORMAL#TBK_RESPUESTA=0#TBK_MONTO=10000#TBK_CODIGO_AUTORIZACION=001882#TBK_FINAL_NUMERO_TARJETA=9509#TBK_FECHA_CONTABLE=0123#TBK_FECHA_TRANSACCION=0123#TBK_HORA_TRANSACCION=150959#TBK_ID_SESION=430c2c85#TBK_ID_TRANSACCION=2164532727#TBK_TIPO_PAGO=VD#TBK_NUMERO_CUOTAS=0#TBK_VCI=TSY

With all the details from the payment

Fourth message

Finally you have to acknowledge this POST request with an encrypted ACK message. Once done, Webpay will capture the payment and redirect the user to the TBK_URL_EXITO url if the value of TBK_RESPUESTA is zero and to TBK_URL_FRACASO otherwise.

Hope this can help anybody trying to implement the protocol on any language and any comment or doubt can be done on the comments..