docusealco/docuseal · error · WebhookUrls::Signatures::InvalidSignatureError

InvalidSignatureError

Error message

InvalidSignatureError

What it means

Raised by WebhookUrls::Signatures.verify when the signature header cannot be parsed into a timestamp and an HMAC: header.to_s.split('.', 2) must yield a decimal-integer timestamp and a signature part. A missing header, a header with no '.', an empty part, or a non-numeric timestamp raises InvalidSignatureError before any HMAC comparison. (The same error class is raised later — line 35 — when the HMAC itself does not match.)

Source

Thrown at lib/webhook_urls/signatures.rb:26

    InvalidSignatureError = Class.new(StandardError)
    TimestampError = Class.new(StandardError)

    module_function

    def generate_secret
      SECRET_PREFIX + Base64.strict_encode64(SecureRandom.bytes(SECRET_BYTES))
    end

    def sign(secret, body:, timestamp: Time.current.to_i)
      "#{timestamp}.#{OpenSSL::HMAC.hexdigest('sha256', secret, "#{timestamp}.#{body}")}"
    end

    def verify(secret, body:, header:, tolerance: TOLERANCE)
      ts, sig = header.to_s.split('.', 2)
      ts = Integer(ts, exception: false)

      raise InvalidSignatureError unless ts && sig

      now = Time.current.to_i

      raise TimestampError, 'Too old' if ts < now - tolerance
      raise TimestampError, 'In future' if ts > now + tolerance

      expected = OpenSSL::HMAC.hexdigest('sha256', secret, "#{ts}.#{body}")

      raise InvalidSignatureError unless ActiveSupport::SecurityUtils.secure_compare(expected, sig)

      true
    end
  end
end

View on GitHub (pinned to 004a22c1c8)

Solutions

  1. Produce the header with WebhookUrls::Signatures.sign(secret, body:) — it returns 'timestamp.hexdigest' ready to ship
  2. Send that exact string in the header the receiver reads; do not re-encode, truncate, or reorder it
  3. If signing by hand: "#{Time.now.to_i}.#{OpenSSL::HMAC.hexdigest('sha256', secret, "#{ts}.#{body}")}"
  4. On the receiver, check the header shape before verifying: it must match /\A\d+\.[0-9a-f]{64}\z/

Example fix

# before
header = OpenSSL::HMAC.hexdigest('sha256', secret, body) # digest only -> InvalidSignatureError
# after
header = WebhookUrls::Signatures.sign(secret, body: body) # '1737561600.5f3a...' (ts.hmac)
Defensive patterns

Strategy: try-catch

Validate before calling

def plausible_signature_header?(header)
  header.to_s.match?(/\A\d+\.[0-9a-f]{64}\z/)
end
return head :unauthorized unless plausible_signature_header?(request.headers['Webhook-Signature'])

Type guard

def signed_header?(header)
  !header.to_s.match(/\A(\d+)\.([0-9a-f]{64})\z/).nil?
end

Try / catch

begin
  WebhookUrls::Signatures.verify(secret, body: raw_request_body, header: request.headers['Webhook-Signature'])
rescue WebhookUrls::Signatures::InvalidSignatureError
  head :unauthorized # reject: malformed or mismatching signature; never process the payload
end

Prevention

When it happens

Trigger: Calling verify(secret, body:, header: nil), header: '', header: '<hex-only>' (no timestamp prefix), or 'abc.signature' where 'abc' is not an integer. The expected format is exactly '<unix_seconds>.<hex_sha256_hmac>', as produced by WebhookUrls::Signatures.sign.

Common situations: Receiver reads the wrong header name and passes an empty string; sender transmits only the HMAC digest without the timestamp prefix; a proxy/gateway strips or URL-encodes the header; components reversed (hex first, timestamp second); timestamp sent in milliseconds or ISO-8601 instead of unix seconds.

Related errors


AI-assisted analysis of docusealco/docuseal@004a22c1c8 (2026-08-21). Data as JSON: /api/errors/d415aa898d6e8ba9. Report an issue: GitHub.