Module: Cloudflare::EmailService::Inbound

Defined in:
lib/cloudflare/email_service/inbound.rb

Overview

Verifies the HMAC-SHA256 signature a Cloudflare Email Worker attaches to a forwarded inbound message. The Worker signs ‘“<timestamp>.<raw body>”` with a shared secret and sends the timestamp and hex digest as headers; this recomputes the digest, compares it in constant time, and rejects stale timestamps to block replays.

Pure and Rails-free (stdlib OpenSSL only) so it can be unit-tested on its own; the Action Mailbox ingress is a thin wrapper around Inbound.verify.

Constant Summary collapse

REPLAY_WINDOW =

Reject timestamps more than this many seconds from now (either side).

300

Class Method Summary collapse

Class Method Details

.secure_compare(expected, actual) ⇒ Object

Constant-time string comparison. Bails early on a length mismatch, which the digest’s fixed width makes safe to leak.



37
38
39
40
41
# File 'lib/cloudflare/email_service/inbound.rb', line 37

def secure_compare(expected, actual)
  return false unless expected.bytesize == actual.bytesize

  OpenSSL.fixed_length_secure_compare(expected, actual)
end

.verify(secret:, timestamp:, signature:, body:, now: Time.now.to_i) ⇒ Symbol

Returns :ok, :stale (timestamp outside the window), or :bad_signature (missing/empty input or digest mismatch).

Returns:

  • (Symbol)

    :ok, :stale (timestamp outside the window), or :bad_signature (missing/empty input or digest mismatch).



23
24
25
26
27
28
29
30
31
32
33
# File 'lib/cloudflare/email_service/inbound.rb', line 23

def verify(secret:, timestamp:, signature:, body:, now: Time.now.to_i)
  return :bad_signature if [secret, timestamp, signature, body].any? { |v| v.to_s.empty? }
  return :stale if (now - timestamp.to_i).abs > REPLAY_WINDOW

  # Build the signed payload in binary: raw RFC822 bodies carry bytes > 127
  # (8bit transfer encoding, binary attachments), which would raise
  # Encoding::CompatibilityError if interpolated into a UTF-8 string.
  signed = "#{timestamp}.".b + body.to_s.b
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret.to_s, signed)
  secure_compare(expected, signature.to_s) ? :ok : :bad_signature
end