HMAC and Digital Signatures
HMAC stands for Hash-based Message Authentication Code, it is a symmetric, which mean there is only one key (for example API secret key) that both parties use to hash and verify messages. Digital Signatures is asymmetric, which mean there is two keys (a keypair). The sender keeps the private key and the receiver keeps the public key. The private key is used to sign the message, and the public key is used to verify it.
Let’s see how HMAC is used in Ruby in the following example.
require 'openssl'
def prepare_message(request_body, nonce, api_key)
# Concatenate the request body with the nonce
message = request_body + nonce.to_s
# Compute the HMAC with SHA256
OpenSSL::HMAC.hexdigest('sha256', api_key, message)
end
def verify_message(request_body, nonce, signature, api_key)
# Generate the expected signature
expected_signature = prepare_message(request_body, nonce, api_key)
# Compare the computed HMAC with the signature
expected_signature == signature
end
# Example usage:
request_body = '{"hello": "world"}'
nonce = 1243549809
api_key = 'sk_test_secret'
# Prepare the message signature
signature = prepare_message(request_body, nonce, webhook_key)
puts "Generated Signature: #{signature}"
# Verify the signature
if verify_message(request_body, nonce, signature, webhook_key)
puts "Signature verified!"
else
puts "Signature verification failed."
end
In second example, I implemented Digital Signatures in Ruby. We will see that, instead of using a single secret key
as in the first example, we use a keypair
: public key
and private key
(The library changes from OpenSSL::HMAC
to OpenSSL::PKey
).
require 'openssl'
# Generate a new RSA key pair
private_key = OpenSSL::PKey::RSA.new(2048)
# Get the public key
public_key = key.public_key
def prepare_message(request_body, timestamp, private_key)
# Concatenate the request body with the timestamp
message = request_body + timestamp.to_s
# Create a digest (SHA256 is commonly used)
digest = OpenSSL::Digest::SHA256.new
# Sign the message with the private key
signature = private_key.sign(digest, message)
# Convert binary format to hexadecimal format for human readable
signature.unpack1('H\*')
end
def verify_message(request_body, timestamp, hex_signature, public_key)
# Concatenate the request body with the timestamp
message = request_body + timestamp.to_s
# Create a digest (SHA256 is commonly used)
digest = OpenSSL::Digest::SHA256.new
# Convert hexadecimal to binary before verify
signature_bytes = [hex_signature].pack('H\*')
# Verify the signature with the public key
public_key.verify(digest, signature_bytes, message)
end
# Example usage:
request_body = '{"hello": "world"}'
timestamp = Time.now.to_i # Get current timestamp as an integer
# Prepare the message signature
hex_signature = prepare_message(request_body, timestamp, private_key)
puts "Generated Signature: #{hex_signature}" # Print signature in hex format
# Verify the signature
if verify_message(request_body, timestamp, hex_signature, public_key)
puts "Signature verified!"
else
puts "Signature verification failed."
end
In the second example, I changed nonce
to timestamp
to reflect the preferences of different companies when design their APIs.
It is also important to note that private_key.sign
returns a binary
output instead of hex
, unlike OpenSSL::HMAC.hexdigest
. Therefore, we need to unpack
it to hexadecimal
for human readability before transferring the data, and pack
it back to binary
before verifying.