Module: Increase::Webhook::Signature

Defined in:
lib/increase/webhook/signature.rb

Constant Summary collapse

DEFAULT_TIME_TOLERANCE =

300 seconds (5 minutes)

300
DEFAULT_SCHEME =
"v1"

Class Method Summary collapse

Class Method Details

.compute_signature(timestamp:, payload:, secret:) ⇒ Object



50
51
52
53
# File 'lib/increase/webhook/signature.rb', line 50

def self.compute_signature(timestamp:, payload:, secret:)
  signed_payload = timestamp.to_s + "." + payload.to_s
  OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
end

.verify?(payload:, signature_header:, secret:, scheme: DEFAULT_SCHEME, time_tolerance: DEFAULT_TIME_TOLERANCE) ⇒ Boolean

Returns:

  • (Boolean)


11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/increase/webhook/signature.rb', line 11

def self.verify?(payload:, signature_header:, secret:, scheme: DEFAULT_SCHEME, time_tolerance: DEFAULT_TIME_TOLERANCE)
  # Helper for raising errors with additional metadata
  sig_error = ->(msg) do
    WebhookSignatureVerificationError.new(msg, signature_header: signature_header, payload: payload)
  end

  # Parse header
  sig_values = signature_header.split(",").map { |pair| pair.split("=") }.to_h

  # Extract values
  t = sig_values["t"] # Should be a string (ISO-8601 timestamp)
  sig = sig_values[scheme]
  raise sig_error.call("No timestamp found in signature header") if t.nil?
  raise sig_error.call("No signature found with scheme #{scheme} in signature header") if sig.nil?

  # Check signature
  expected_sig = compute_signature(timestamp: t, payload: payload, secret: secret)
  matches = Util.secure_compare(expected_sig, sig)
  raise sig_error.call("Signature mismatch") unless matches

  # Check timestamp tolerance to prevent timing attacks
  if time_tolerance > 0
    begin
      timestamp = DateTime.parse(t)
      now = DateTime.now
      diff = (now - timestamp) * 24 * 60 * 60 # in seconds

      # Don't allow timestamps in the future
      if diff > time_tolerance || diff < 0
        raise sig_error.call("Timestamp outside of the tolerance zone")
      end
    rescue Date::Error
      raise sig_error.call("Invalid timestamp in signature header: #{t}")
    end
  end

  true
end