4
votes

I'm looking to learn how to verify a Paddle webhook with Ruby? Their example has an option on how to do it with PHP, Python and JavaScript, but no Ruby. Any ideas on how to do it?

This following old example doesn't work:

require 'base64'
require 'php_serialize'
require 'openssl'


public_key = '-----BEGIN PUBLIC KEY-----
MIICIjANBgkqh...'

# 'data' represents all of the POST fields sent with the request.
# Get the p_signature parameter & base64 decode it.
signature = Base64.decode64(data['p_signature'])

# Remove the p_signature parameter
data.delete('p_signature')

# Ensure all the data fields are strings
data.each {|key, value|data[key] = String(value)}

# Sort the data
data_sorted = data.sort_by{|key, value| key}

# and serialize the fields
# serialization library is available here: https://github.com/jqr/php-serialize
data_serialized = PHP.serialize(data_sorted, true)

# verify the data
digest    = OpenSSL::Digest::SHA1.new
pub_key   = OpenSSL::PKey::RSA.new(public_key).public_key
verified  = pub_key.verify(digest, signature, data_serialized)

if verified
    puts "Yay! Signature is valid!"
else
    puts "The signature is invalid!"
end

Here is their example in JS:

// Node.js & Express implementation
const express = require('express');
const querystring = require('querystring');
const crypto = require('crypto');
const Serialize = require('php-serialize');

const router = express.Router();
const pubKey = `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`

function ksort(obj){
    let keys = Object.keys(obj).sort();
    let sortedObj = {};

    for (var i in keys) {
      sortedObj[keys[i]] = obj[keys[i]];
    }

    return sortedObj;
  }

function validateWebhook(jsonObj) {
    const mySig = Buffer.from(jsonObj.p_signature, 'base64');
    delete jsonObj.p_signature;
    // Need to serialize array and assign to data object
    jsonObj = ksort(jsonObj);
    for (var property in jsonObj) {
        if (jsonObj.hasOwnProperty(property) && (typeof jsonObj[property]) !== "string") {
            if (Array.isArray(jsonObj[property])) { // is it an array
                jsonObj[property] = jsonObj[property].toString();
            } else { //if its not an array and not a string, then it is a JSON obj
                jsonObj[property] = JSON.stringify(jsonObj[property]);
            }
        }
    }
    const serialized = Serialize.serialize(jsonObj);
    // End serialize data object
    const verifier = crypto.createVerify('sha1');
    verifier.update(serialized);
    verifier.end();

    let verification = verifier.verify(pubKey, mySig);

    if (verification) {
        return 'Yay! Signature is valid!';
    } else {
        return 'The signature is invalid!';
    }
}

/* Validate a Paddle webhook to this endpoint, or wherever in your app you are listening for Paddle webhooks */
router.post('/', function(req, res, next) {
    res.send(validateWebhook(req.body));
});

module.exports = router;

How could I verify the webhook with Ruby? Is there an alternative way to verify the webhook?

Here is an example webhook request:

(
    [alert_name] => subscription_created
    [cancel_url] => https://checkout.paddle.com/subscription/cancel?user=4&subscription=8&hash=b0bd354fexamplec39b0ff93c917804acf
    [checkout_id] => 1-61ff5b400-756ea301a9
    [currency] => USD
    [email] => [email protected]
    [event_time] => 2019-08-10 18:33:58
    [marketing_consent] => 
    [next_bill_date] => 2019-08-18
    [passthrough] => 1132
    [quantity] => 67
    [status] => active
    [subscription_id] => 4
    [subscription_plan_id] => 5
    [unit_price] => unit_price
    [update_url] => https://checkout.paddle.com/subscription/update?user=5&subscription=4&hash=e937ed03f1637e45d912f4f4d293a
    [user_id] => 6
    [p_signature] => HM2Isn1k6Sy1cKySQGoFH...
)

EDIT:

I'm using Ruby 2.5.5 and Ruby on Rails 5. Currently still getting always "false" in the end. I'll go through it on my console:

Here is the (fake) data, that I get in Rails:

data = {
"alert_id"=>"1", 
"alert_name"=>"alert_created", 
"cancel_url"=>"https://...", 
"checkout_id"=>"1", 
"user_id"=>"1", 
"p_signature"=>"fwWXqR9C..."
} 

public_key = '-----BEGIN PUBLIC KEY-----sDFKJSD2332FKJLWJF......'

Then I do the following:

signature = Base64.decode64(data['p_signature'])

data.delete('p_signature')

data.each {|key, value|data[key] = String(value)}

data_sorted = data.sort_by{|key, value| key}

data_serialized = data_sorted.to_json

digest    = OpenSSL::Digest::SHA1.new

pub_key   = OpenSSL::PKey::RSA.new(public_key)

verified  = pub_key.verify(digest, signature, data_serialized)

In the end verified is always false. What am I doing wrong?

2

2 Answers

3
votes

The Ruby example that you mentioned doesn't work because you need get the data variable. This must be sent from controller to some class that process the request.

Try this:

in routes.rb

get 'check', to: 'test#check'

in controller

class TestController < ApplicationController

  def check
    verification = SignatureVerifier.new(check_params.as_json)
    if verification
      #... do something
    end
  end

  private

  def check_params
    params.permit.all
  end
end

in verifier class

require 'base64'
require 'json'
require 'openssl'

class SignatureVerifier

  def initialize(data)
    @data = data
    @public_key_path = '/path/to/file'
  end

  #data = {
  #  "alert_name": "payment_succeeded",
  #  "balance_currency": "USD",
  #  "balance_earnings": 355.05,
  #  "balance_fee": 177.36,
  #  "balance_gross": 180.85,
  #  "balance_tax": 433.43,
  #  "checkout_id": "4-601ee0e3d793922-ab8910b010",
  #  "currency": "USD",
  #  "customer_name": "customer_name",
  #  "earnings": 292.87,
  #  "product_name": "Example",
  #  "quantity": 12,
  #  "p_signature": "dl8PN7OrxiYHSJzT3CLUDlElodOE2j8puZkDNPHX9rZnTgig123f4KhtUXZT/HjbU5D7g/PZggxSCt9YrMcWrbSkfINJROTb+YrlhYKAVyTbmMWJV8u+YU6VcGNkhcGK7tIZNBJuaKMBrByrYA14gR3TvMjgXbQWNSFJ8LgJKMWoovbpuOkQwzkKze4vavt3WhElW0izPZwpiqVWTVXAlIvDxHTNT+sS1jXqAHdoli6sVblQQtAujSxdGm2OXB92yifcV0oHhrsqt8rCk1TzJOqsVrhQz1lqSYsbdhlM40QPHM7nHPGe5RITly4t8BjsuCB+V1aeof3N5A0ZDk+2M2Cox6S+vEahEdbW8QdecIKN12SMAYI5kx9zMMiUZ9XZqqC6orXE3uVAcTvMwiTRDDmEVr1HtsBZRo/Ykg7+fMYPc/o7rDpA16/EIOcce1zp+vgilL6rSxIuMFfWlP9qxzrV1MtcmQa86NxEU0GJtebkhehXZfh/eDCAjysmrrBM5xkqE19M+Ye4jZCRTzQTHyDJxjdNYefk7bVfivwRI606JJCGYUMTD6NIsn4rinw2SxKkZquqjTykcob5gn3HH+0AxyjuDj7fsLyqEl3gE9tgo/oMKRBy+zsYzQk4v291sh2PbUfH36W4aL4YYztlsarfMIBWqJshc8rf0RL3pAM="
  #}

  def verify
    data = @data
    signature = Base64.decode64(data[:p_signature])

    # Remove the p_signature parameter
    data.delete(:p_signature)

    # Ensure all the data fields are strings
    data.each {|key, value|data[key] = String(value)}

    # Sort the data
    data_sorted = data.sort_by{|key, value| key}

    # Serialized with JSON library
    data_serialized = data_sorted.to_json

    # verify the data
    digest    = OpenSSL::Digest::SHA1.new
    pub_key   = OpenSSL::PKey::RSA.new(File.read(@public_key_path))
    verified  = pub_key.verify(digest, signature, data_serialized)

    verified
  end
end
0
votes

In case this is of help to anybody in the future. The following code solved the problems for me.

[1] Add this to the controller class that handles your APIs integrations/webhooks:

  # main Paddle end-point for paddle generated webhook events
  def paddle
   
    if PaddleWebhooks.verify_paddle_authenticity(params.as_json)
        msg= 'Yay! Signature is valid!:' + params.to_s
    else
        msg= 'The signature is invalid!:' + params.to_s
    end
    
    render json: msg.to_json, status: :ok
  end

Paddle expect webhooks handlers to return status code 200 (:ok). We also add msg to be returned to Paddle's webhook simulator so you can check what you receive from them (and its structure). Once you move into production you of course can delete that.

[2] Add the php_serialize gem to your gemfile

# Use for verifying Paddle's webhooks
gem "php-serialize"

[3] Create a support class:

class PaddleWebhooks
  require 'base64'
  require 'php_serialize'
  require 'openssl'

  def self.verify_paddle_authenticity(data)
       
    // PADDLE_PUBLIC_KEY should have your paddle provided key as a single line
    // and without the  "-----BEGIN PUBLIC KEY-----" and 
    // "-----END PUBLIC KEY-----"  strings
    public_key =ENV['PADDLE_PUBLIC_KEY']
      
    signature = Base64.decode64(data['p_signature'])

    # Remove the p_signature parameter as per Paddle's instructions
    data.delete('p_signature')
    # Remove also from the data the controller and action (key,value) pairs
    data.delete('controller')
    data.delete('action')

    # Ensure all the data fields are strings
    data.each {|key, value|data[key] = String(value)}

    # Sort the data
    data_sorted = data.sort_by{|key, value| key}

    # Serialized with JSON library
    data_serialized = PHP.serialize(data_sorted, true)

    # verify the data
    digest    = OpenSSL::Digest::SHA1.new
    pub_key = OpenSSL::PKey::RSA.new(Base64.decode64(public_key))
    verified  = pub_key.verify(digest, signature, data_serialized)

    verified  
  end


end