How to verify a webhook with Ruby? - ruby-on-rails

I'm using Ruby 2.5.5 and Rails 5.2. I'm trying to verify a Paddle webhook, but can't get the verification to work. I tried various ways, here is one of them. I'll go through it on my console:
Here is the data:
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-----
FAKEdfsdfsdfsdfsdfsdfsdf23423423423423KCAgEAnyBxU0cUW9NGQU1Ur0MD
/M+VxdMsv7PMXsZFAKEKEYlskdfjlskdjflsjlskdj8i7+D9xZT57VhIfv8hInGy
IQFmJRrWxgmHjSW4cKbjg6Qg0NgOEuPdEIixsQZ/AfIar4KNObDul0EXL92B6ru/
...
-----END PUBLIC KEY-----"
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? I asked a related question here, but couldn't get it yet to work.

Example in paddle documentation that you've linked to suggests serializing data to PHP format before verification using php-serialize gem, that differs from regular json:
# ... data = ...
data_sorted.to_json
=> "[[\"alert_id\",\"1\"],[\"alert_name\",\"alert_created\"],[\"cancel_url\",\"https://...\"],[\"checkout_id\",\"1\"],[\"user_id\",\"1\"]]"
PHP.serialize(data_sorted, true)
=> "a:5:{s:8:\"alert_id\";s:1:\"1\";s:10:\"alert_name\";s:13:\"alert_created\";s:10:\"cancel_url\";s:11:\"https://...\";s:11:\"checkout_id\";s:1:\"1\";s:7:\"user_id\";s:1:\"1\";}"
as you can see - it gives different result, so obviously signature will not match.
Actually, you can take their whole ruby example:
require 'base64'
require 'php_serialize' # https://github.com/jqr/php-serialize
require 'openssl'
public_key = '-----BEGIN PUBLIC KEY-----
MIICIjANBgkqh...'
# 'data' represents all of the POST fields sent with the request.
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 = PHP.serialize(data_sorted, true)
digest = OpenSSL::Digest::SHA1.new
pub_key = OpenSSL::PKey::RSA.new(public_key).public_key
verified = pub_key.verify(digest, signature, data_serialized)

I can't explain it completely but I think the false is related to your signature or data_serialized parameter, I use the same digest. Another difference is that sign is called on the key in the example below:
verify(digest, signature, data)
from: https://ruby-doc.org/stdlib-2.6.3/libdoc/openssl/rdoc/OpenSSL/PKey/PKey.html
To verify the String signature, digest, an instance of OpenSSL::Digest, must be provided to re-compute the message digest of the original data, also a String. The return value is true if the signature is valid, false otherwise. A PKeyError is raised should errors occur. Any previous state of the Digest instance is irrelevant to the validation outcome, the digest instance is reset to its initial state during the operation.
Example:
data = 'Sign me!'
digest = OpenSSL::Digest::SHA256.new
pkey = OpenSSL::PKey::RSA.new(2048)
signature = pkey.sign(digest, data)
pub_key = pkey.public_key
puts pub_key.verify(digest, signature, data) # => true`

Related

How do I manually set cookie in rails app?

I have a situation where I need to send session id in custom header because safari does not allow sending cookie from iframe.
But now, I'm having trouble setting the cookie from rack middleware.
status, headers, body = #app.call(env)
session_id = headers['X-SESSION-ID'] || headers['HTTP_X_SESSION_ID'] || headers['x-session-id'] || ''
if !headers['Cookie'].present? && session_id.present?
headers['Cookie'] = {
'_session_id': {
value: session_id,
path: '/',
httpOnly: true
}
}
end
Or is there way to manually set session id without having to fetch it from cookie?
Update 1:
Modifying rack request files does work. But, I'm not sure how to proceed with this one. If nothing works then, I might manually update this file in all servers.
def cookies
hash = #env["rack.request.cookie_hash"] ||= {}
string = #env["HTTP_COOKIE"] || "_session_id=#{#env["HTTP_X_SESSION_ID"]}"
return hash if string == #env["rack.request.cookie_string"]
hash.clear
# According to RFC 2109:
# If multiple cookies satisfy the criteria above, they are ordered in
# the Cookie header such that those with more specific Path attributes
# precede those with less specific. Ordering with respect to other
# attributes (e.g., Domain) is unspecified.
cookies = Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
#env["rack.request.cookie_string"] = string
hash
end
Looks like modifying the cookie method as above do not work.
Finally I managed to solve it. So, I added rack_request.rb in initializers. And here's the code for it:
require 'rack'
require 'rack/request'
require 'rack/utils'
Rack::Request.class_eval do
def cookies
hash = #env["rack.request.cookie_hash"] ||= {}
string = #env["HTTP_COOKIE"] || "_session_id=#{#env['HTTP_X_SESSION_ID']}"
unless string =~ /\s*_session_id=/i
if #env['HTTP_X_SESSION_ID'].present?
string << "; _session_id=#{#env['HTTP_X_SESSION_ID']}"
end
end
# require 'colorize'
#
# Rails.logger.info 'from cookies'.green
# Rails.logger.info (string.blue)
return hash if string == #env["rack.request.cookie_string"]
hash.clear
# According to RFC 2109:
# If multiple cookies satisfy the criteria above, they are ordered in
# the Cookie header such that those with more specific Path attributes
# precede those with less specific. Ordering with respect to other
# attributes (e.g., Domain) is unspecified.
cookies = Rack::Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
cookies.each { |k, v| hash[k] = Array === v ? v.first : v }
#env["rack.request.cookie_string"] = string
hash
end
end
And I'm sending 'X-SESSION-ID' in my ajaxHeaders for session id.

AWS signature version 4 signing key issue ruby

Has to be done in ruby
my code is:
require "openssl"
require "time"
require "uri"
require "pathname"
def getSignatureKey (key, dateStamp, regionName, serviceName)
digest= OpenSSL::Digest.new('sha256')
kDate = OpenSSL::HMAC.hexdigest(digest, ("AWS4" + key), dateStamp)
puts("kdate: #{kDate}")
kRegion = OpenSSL::HMAC.hexdigest(digest, kDate, regionName)
puts("kRegion: #{kRegion}")
kService = OpenSSL::HMAC.hexdigest(digest, kRegion, serviceName)
puts("kService: #{kService}")
kSigning = OpenSSL::HMAC.hexdigest(digest, kService, "aws4_request")
puts("kSigning: #{kSigning}")
kSigning
end
key = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
dateStamp = '20120215'
regionName = 'us-east-1'
serviceName = 'iam'
puts("===========================================OUTPUT IS: ==============================================")
getSignatureKey(key,dateStamp,regionName,serviceName)
which produces the output:
kdate: 969fbb94feb542b71ede6f87fe4d5fa29c789342b0f407474670f0c2489e0a0d
kRegion:a59e30f9d899c47b3dd68ea1c0ab3bb529e03a8f4ed2f54cb64af547330a22a0
kService:e33cbcda009243ed24c04af249a477d33b33c97ba7717327636afab2c6e6ef78
kSigning:9d8a8749deaee911a4ed0d04fb34af903f690d0423c146023bd282ba8ebf955b
however the output should be, according to aws documentation: https://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html):
kdate: 969fbb94feb542b71ede6f87fe4d5fa29c789342b0f407474670f0c2489e0a0d
kRegion:69daa0209cd9c5ff5c8ced464a696fd4252e981430b10e3d3fd8e2f197d7a70c
kService:f72cfd46f26bc4643f06a11eabb6c0ba18780c19a8da0c31ace671265e3c87fa
kSigning:f4780e2d9f65fa895f9c67b32ce1baf0b0d8a43505a000a1a9e090d414db404d
so the kdate matches but the others do not, I haven't miss-spelled "us-east-1" so I'm really not sure why they don't match. If anyone could offer advice I'd be very grateful.

Converting python to ruby params issue

I'm converting some python code to ruby. It's going ok so far, except I'm running into some issues with parameters. The python code is:
def sign_policy(policy):
signed_policy = base64.b64encode(policy)
signature = base64.b64encode(hmac.new(
app.config.get('AWS_CLIENT_SECRET_KEY'), signed_policy, hashlib.sha1).
digest())
return { 'policy': signed_policy, 'signature': signature }
def sign_headers(headers):
headers = bytearray(headers, 'utf-8') # hmac doesn't want unicode
return {
'signature': base64.b64encode(hmac.new(
app.config.get('AWS_CLIENT_SECRET_KEY'), headers, hashlib.sha1).
digest())
}
def s3_signature():
request_payload = request.get_json()
if request_payload.get('headers'):
response_data = sign_headers(request_payload['headers'])
else:
response_data = sign_policy(request.data)
return jsonify(response_data)
My ruby version so far is:
def create
puts params[:headers]
if params[:headers].present?
response_data = sign_headers(params[:headers])
else
response_data = sign_policy(params)
end
render json: response_data
end
private
def sign_policy(policy)
signed_policy = Base64.encode64(policy).gsub("\n","")
signature = Base64.encode64(
OpenSSL::HMAC.digest(
OpenSSL::Digest.new('sha1'),
AWS_SECRET_KEY, signed_policy)
).gsub("\n","")
return { 'policy': signed_policy, 'signature': signature }
end
def sign_headers(headers)
#headers = bytearray(headers, 'utf-8')
return {
'signature': Base64.encode64(
OpenSSL::HMAC.digest(
AWS_SECRET_KEY, headers, OpenSSL::Digest.new('sha1')
))
}
end
I'm running into the following issue: no implicit conversion of ActionController::Parameters into String, which makes it obvious whats wrong (Params is a hash and it needs to be a string)..However, what is being passed in the python code? I'm missing what I should be passing?
Most probably you need to pass a single value, String or any other, it depends on the data type you need to pass to use Base64.encode64(policy).
As you're passing params[:headers] in the sign_headers method call, in the case of the sign_policy call, you're passing params which is the whole ActionController::Parameters, try debugging the values you sent on params to send the needed value.

Using HMAC SHA256 in Ruby

I'm trying to apply HMAC-SHA256 for generate a key for an Rest API.
I'm doing something like this:
def generateTransactionHash(stringToHash)
key = '123'
data = 'stringToHash'
digest = OpenSSL::Digest.new('sha256')
hmac = OpenSSL::HMAC.digest(digest, key, data)
puts hmac
end
The output of this is always this: (if I put '12345' as parameter or 'HUSYED815X', I do get the same)
ۯw/{o���p�T����:��a�h��E|q
The API is not working because of this... Can some one help me with that?
According to the documentation OpenSSL::HMAC.digest
Returns the authentication code an instance represents as a binary string.
If you have a problem using that maybe you need a hex encoded form provided by OpenSSL::HMAC.hexdigest
Example
key = 'key'
data = 'The quick brown fox jumps over the lazy dog'
digest = OpenSSL::Digest.new('sha256')
OpenSSL::HMAC.digest(digest, key, data)
#=> "\xF7\xBC\x83\xF40S\x84$\xB12\x98\xE6\xAAo\xB1C\xEFMY\xA1IF\x17Y\x97G\x9D\xBC-\x1A<\xD8"
OpenSSL::HMAC.hexdigest(digest, key, data)
#=> "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"
Try This:
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, data)
def make_payment(user)
#key= SecureRandom.hex(10)
#puts #key
#secret_key = #key
puts " this is the public key #{#secret_key}"
#access_key= generate_key
puts " this is the access key #{#access_key}"
#name= #user.name
puts "#{#name}"
#time= Time.now.in_time_zone("Nairobi")
puts "This is the time request sent #{#time}"
#server_key = SecureRandom.base64
puts "This is the server key #{#server_key}"
#data = 'This request is being made from Learnida for users to make a payment'
#digest = OpenSSL::Digest.new('sha256')
uri = URI.parse("https://learnida.com")
#hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), #secret_key, #access_key)
puts "This is the HMAC #{#hmac}"
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "TM-HMAC-SHA256 key=#{#access_key} ts=#{#time} sign=#{#hmac}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
#hmacdigest= OpenSSL::HMAC.digest(#digest, #server_key, #data)
puts" This is the HMAC:SHA-256: #{#hmacdigest}"
#puts res.body
#=> "\xF7\xBC\x83\xF40S\x84$\xB12\x98\xE6\xAAo\xB1C\xEFMY\xA1IF\x17Y\x97G\x9D\xBC-\x1A<\xD8"
#sslkey= OpenSSL::HMAC.hexdigest(#digest, #server_key, #data)
puts #sslkey
In my case (Ticketmatic) I had to create the HMAC like above and add an Authorization header to the request with the HMAC in it.
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret_key, access_key + name + time)
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "TM-HMAC-SHA256 key=#{access_key} ts=#{time} sign=#{hmac}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
You can find a full gist here
And a blogpost with more explantion here

How to pass Json parameter for blockchain-wallet ruby api methods

Trying to work on blockchain-wallet ruby api methods as in doc https://github.com/Tolsi/blockchain-wallet-ruby/blob/master/lib/blockchain/wallet.rb where am stuck with passing Json parameters for payment and sendmany methods.
recipients = {"1H8fsmAgVhUt567I65hGJPkMbnCCy5": "1000000"}
wallet = Blockchain::Wallet.new("b14d36af-2fa6-4d76-91b5-ff70b8370ec7", "n$grJ74hAshfggnb6864%j78##^hChttp://ya.ru#contentpurl", nil)
wallet.sendmany(recipients, from = nil, shared = nil, fee = nil, note = nil)
Got error despite substituting the required,
Invalid Recipients JSON. Please make sure it is url encoded and consult the docs. (WebAPI::WebApiException)
How to solve the problem ?
In the Blockchain::Wallet gem code, I found this:
def build_url(operation_name, params)
#url + operation_name + "?" + params.map { |param, value| "#{param}=#{value}" if value }.join("&")
end
I believe the way it's being built, you will need to transform in JSON and also encode your recipients string:
require 'json'
require 'uri'
recipients = {"1H8fsmAgVhUt567I65hGJPkMbnCCy5" => "1000000"}
wallet = Blockchain::Wallet.new("b14d36af-2fa6-4d76-91b5-ff70b8370ec7", "n$grJ74hAshfggnb6864%j78##^hChttp://ya.ru#contentpurl", nil)
wallet.sendmany(URI.encode(JSON.generate(recipients)))
The recipient will be "%7B%221H8fsmAgVhUt567I65hGJPkMbnCCy5%22:%221000000%22%7D".
References:
https://github.com/Tolsi/blockchain-wallet-ruby/blob/master/lib/blockchain/web_api.rb
https://blockchain.info/api/blockchain_wallet_api
UPDATE: Try remove the URI.encode if this does not work.

Resources