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

Raises:

  • (ArgumentError)


60
61
62
63
64
65
66
67
# File 'lib/increase/webhook/signature.rb', line 60

def self.compute_signature(timestamp:, payload:, secret:)
  raise ArgumentError, "timestamp is required" if timestamp.nil?
  raise ArgumentError, "payload is required" if payload.nil?
  raise ArgumentError, "secret is required" if secret.nil?

  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) ⇒ Object

Raises a WebhookSignatureVerificationError if the signature is invalid



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
49
50
51
52
53
54
55
56
57
58
# File 'lib/increase/webhook/signature.rb', line 19

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
  raise sig_error.call("Webhook secret is required") if secret.nil?
  raise sig_error.call("Payload is required") if payload.nil?
  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

.verify?(**args) ⇒ Boolean

Verifies the signature of a webhook payload (without raising an error)

Returns:

  • (Boolean)


12
13
14
15
16
# File 'lib/increase/webhook/signature.rb', line 12

def self.verify?(**args)
  verify(**args)
rescue WebhookSignatureVerificationError
  false
end