module Stripe::Webhook::Signature
def self.compute_signature(timestamp, payload, secret)
Computes a webhook signature given a time (probably the current time),
def self.compute_signature(timestamp, payload, secret) raise ArgumentError, "timestamp should be an instance of Time" \ unless timestamp.is_a?(Time) raise ArgumentError, "payload should be a string" \ unless payload.is_a?(String) raise ArgumentError, "secret should be a string" \ unless secret.is_a?(String) timestamped_payload = "#{timestamp.to_i}.#{payload}" OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, timestamped_payload) end
def self.generate_header(timestamp, signature, scheme: EXPECTED_SCHEME)
mainly here for use in test cases (those that are both within this
Note that this isn't needed to verify webhooks in any way, and is
given webhook payload.
Generates a value that would be added to a `Stripe-Signature` for a
def self.generate_header(timestamp, signature, scheme: EXPECTED_SCHEME) raise ArgumentError, "timestamp should be an instance of Time" \ unless timestamp.is_a?(Time) raise ArgumentError, "signature should be a string" \ unless signature.is_a?(String) raise ArgumentError, "scheme should be a string" \ unless scheme.is_a?(String) "t=#{timestamp.to_i},#{scheme}=#{signature}" end
def self.get_timestamp_and_signatures(header, scheme)
Extracts the timestamp and the signature(s) with the desired scheme
def self.get_timestamp_and_signatures(header, scheme) list_items = header.split(/,\s*/).map { |i| i.split("=", 2) } timestamp = Integer(list_items.select { |i| i[0] == "t" }[0][1]) signatures = list_items.select { |i| i[0] == scheme }.map { |i| i[1] } [Time.at(timestamp), signatures] end
def self.verify_header(payload, header, secret, tolerance: nil)
tolerance
- a tolerance is provided and the timestamp is not within the
- no signatures matching the expected signature
- no signatures found with the expected scheme
- the header does not match the expected format
Raises a SignatureVerificationError in the following cases:
Verifies the signature header for a given payload.
def self.verify_header(payload, header, secret, tolerance: nil) begin timestamp, signatures = get_timestamp_and_signatures(header, EXPECTED_SCHEME) # TODO: Try to knock over this blanket rescue as it can unintentionally # swallow many valid errors. Instead, try to validate an incoming # header one piece at a time, and error with a known exception class if # any part is found to be invalid. Rescue that class here. rescue StandardError raise SignatureVerificationError.new( "Unable to extract timestamp and signatures from header", header, http_body: payload ) end if signatures.empty? raise SignatureVerificationError.new( "No signatures found with expected scheme #{EXPECTED_SCHEME}", header, http_body: payload ) end expected_sig = compute_signature(timestamp, payload, secret) unless signatures.any? { |s| Util.secure_compare(expected_sig, s) } raise SignatureVerificationError.new( "No signatures found matching the expected signature for payload", header, http_body: payload ) end if tolerance && timestamp < Time.now - tolerance raise SignatureVerificationError.new( "Timestamp outside the tolerance zone (#{Time.at(timestamp)})", header, http_body: payload ) end true end