Module: Helo::WebhookSignatures

Defined in:
lib/helo/webhook_signatures.rb

Overview

Helpers for verifying the signature on an incoming webhook request.

Defined Under Namespace

Classes: Error, MalformedHeaderError, SignatureMismatchError, TimestampSkewError, UnsupportedVersionError

Constant Summary collapse

SUPPORTED_VERSIONS =

Signing schemes this SDK can verify. The signature header may carry several versions at once (t=...,v1=...,v2=...) so that a new scheme can be rolled out while receivers upgrade; verification uses the newest version present that appears in this list, and ignores the rest.

[1].freeze
TIMESTAMP_VALUE_REGEX =
/\A\d+\z/
SIGNATURE_KEY_REGEX =
/\Av(\d+)\z/
HEX_SIGNATURE_REGEX =
/\A[a-f0-9]+\z/
MAX_TIMESTAMP_SKEW_SECONDS =

5 minutes

300

Class Method Summary collapse

Class Method Details

.generate(payload, key, timestamp) ⇒ String

Compute the hex-encoded HMAC-SHA256 signature for a webhook payload, using the v1 signing scheme.

Parameters:

  • payload (String)

    raw (unparsed) request body

  • key (String)

    signing key for the webhook endpoint

  • timestamp (String)

    unix timestamp in seconds, as sent in the signature header

Returns:

  • (String)


89
90
91
# File 'lib/helo/webhook_signatures.rb', line 89

def generate(payload, key, timestamp)
  OpenSSL::HMAC.hexdigest("SHA256", key, "#{timestamp}.#{payload}")
end

.valid?(signature_header, request_body, signing_key) ⇒ Boolean

Verify a webhook signature header, returning false instead of raising.

Returns:

  • (Boolean)

See Also:

  • #verify!


76
77
78
79
80
# File 'lib/helo/webhook_signatures.rb', line 76

def valid?(signature_header, request_body, signing_key)
  verify!(signature_header, request_body, signing_key)
rescue Error
  false
end

.verify!(signature_header, request_body, signing_key) ⇒ true

Verify a webhook signature header against the raw request body.

Parameters:

  • signature_header (String)

    value of the signature header sent with the webhook

  • request_body (String)

    raw (unparsed) request body

  • signing_key (String)

    signing key for the webhook endpoint

Returns:

  • (true)

    when the signature is valid

Raises:

  • (Error)

    a subclass describing why the signature was rejected



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/helo/webhook_signatures.rb', line 47

def verify!(signature_header, request_body, signing_key)
  timestamp, signatures = parse_header(signature_header)

  version = newest_supported_version(signatures)
  unless version
    raise UnsupportedVersionError,
          "Unsupported webhook signature version: header carries only " \
          "#{signatures.keys.sort.map { |v| "v#{v}" }.join(', ')}"
  end

  skew = (Time.now.to_i - timestamp.to_i).abs
  if skew > MAX_TIMESTAMP_SKEW_SECONDS
    raise TimestampSkewError,
          "Webhook signature timestamp outside tolerance: off by #{skew}s, " \
          "tolerance is #{MAX_TIMESTAMP_SKEW_SECONDS}s"
  end

  computed = signature_for_version(version, request_body, signing_key, timestamp)
  unless signatures.fetch(version).any? { |signature| secure_compare(computed, signature) }
    raise SignatureMismatchError, "Webhook signature mismatch"
  end

  true
end