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
- Produce the header with WebhookUrls::Signatures.sign(secret, body:) — it returns 'timestamp.hexdigest' ready to ship
- Send that exact string in the header the receiver reads; do not re-encode, truncate, or reorder it
- If signing by hand: "#{Time.now.to_i}.#{OpenSSL::HMAC.hexdigest('sha256', secret, "#{ts}.#{body}")}"
- 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
- Always use the library's sign/verify pair instead of hand-rolled HMAC code
- Read the raw request body (not a re-serialized parse) when verifying — body must be byte-identical
- Reject early on malformed headers with a 401 and log the header shape for debugging
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.