Module: Certynix::Webhooks

Defined in:
lib/certynix/webhooks.rb

Overview

Utilitários para validação de assinatura de webhooks Certynix.

Constant Summary collapse

TOLERANCE_SECONDS =

5 minutos

300

Class Method Summary collapse

Class Method Details

.validate_signature(raw_body:, signature:, secret:) ⇒ Hash

Valida a assinatura HMAC-SHA256 de um delivery de webhook.

CRÍTICO: raw_body deve ser o body bruto ANTES de qualquer JSON.parse. Usa OpenSSL.secure_compare (Ruby 2.7+) para constant-time comparison.

Parameters:

  • raw_body (String)

    body bruto do request

  • signature (String)

    valor do header X-Certynix-Signature

  • secret (String)

    signing secret do webhook

Returns:

  • (Hash)

    { type:, payload:, timestamp: }

Raises:



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
59
60
61
62
63
# File 'lib/certynix/webhooks.rb', line 22

def self.validate_signature(raw_body:, signature:, secret:)
  # 1. Parse: "t=timestamp,v1=hash"
  timestamp = nil
  hash      = nil

  signature.split(',').each do |part|
    timestamp = part[2..] if part.start_with?('t=')
    hash      = part[3..] if part.start_with?('v1=')
  end

  if timestamp.nil? || timestamp.empty? || hash.nil? || hash.empty?
    raise WebhookSignatureError, 'Invalid signature format: expected t=timestamp,v1=hash'
  end

  # 2. Anti-replay
  ts   = timestamp.to_i
  diff = (Time.now.to_i - ts).abs
  if diff > TOLERANCE_SECONDS
    raise WebhookReplayError, "Webhook timestamp is #{diff}s old — exceeds #{TOLERANCE_SECONDS}s tolerance"
  end

  # 3. Calcular HMAC-SHA256("{timestamp}.{raw_body}")
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{raw_body}")

  # 4. Constant-time comparison
  unless OpenSSL.secure_compare(expected, hash)
    raise WebhookSignatureError, 'Webhook signature mismatch'
  end

  # 5. Parse payload
  begin
    payload = JSON.parse(raw_body, symbolize_names: true)
  rescue JSON::ParserError
    raise WebhookSignatureError, 'Failed to parse webhook payload as JSON'
  end

  {
    type:      payload[:type].to_s,
    payload:   payload,
    timestamp: ts,
  }
end