class Attio::WebhookUtils::SignatureVerifier

Verifies webhook signatures to ensure payloads are from Attio

def initialize(secret)

def initialize(secret)
  @secret = secret
end

def parse_signature_header(header)

Parse the signature header format: "t=timestamp v1=signature"
def parse_signature_header(header)
  return [nil, nil] unless header
  timestamp = nil
  signature = nil
  header.split(/[,\s]+/).each do |element|
    key, value = element.split("=", 2)
    case key
    when "t"
      timestamp = value
    when "v1"
      signature = value
    end
  end
  [timestamp, signature]
end

def secure_compare(a, b)

Secure string comparison to prevent timing attacks
def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize
  l = a.unpack("C*")
  r = b.unpack("C*")
  result = 0
  l.zip(r) { |x, y| result |= x ^ y }
  result == 0
end

def verify(payload, signature_header, tolerance: TOLERANCE)

Returns:
  • (Boolean) - True if signature is valid

Parameters:
  • tolerance (Integer) -- Maximum age of timestamp in seconds
  • signature_header (String) -- The signature header from the request
  • payload (String) -- The raw request body
def verify(payload, signature_header, tolerance: TOLERANCE)
  timestamp, signature = parse_signature_header(signature_header)
  return false unless timestamp && signature
  # Check timestamp tolerance
  current_time = Time.now.to_i
  if (current_time - timestamp.to_i).abs > tolerance
    return false
  end
  # Generate expected signature
  signed_payload = "#{timestamp}.#{payload}"
  expected_signature = OpenSSL::HMAC.hexdigest("SHA256", @secret, signed_payload)
  # Compare signatures securely
  secure_compare(signature, expected_signature)
end