{"record":{"id":"d415aa898d6e8ba9","repo":"docusealco/docuseal","slug":"invalidsignatureerror","errorCode":null,"errorMessage":"InvalidSignatureError","messagePattern":"InvalidSignatureError","errorType":"exception","errorClass":"WebhookUrls::Signatures::InvalidSignatureError","httpStatus":null,"severity":"error","filePath":"lib/webhook_urls/signatures.rb","lineNumber":26,"sourceCode":"\n    InvalidSignatureError = Class.new(StandardError)\n    TimestampError = Class.new(StandardError)\n\n    module_function\n\n    def generate_secret\n      SECRET_PREFIX + Base64.strict_encode64(SecureRandom.bytes(SECRET_BYTES))\n    end\n\n    def sign(secret, body:, timestamp: Time.current.to_i)\n      \"#{timestamp}.#{OpenSSL::HMAC.hexdigest('sha256', secret, \"#{timestamp}.#{body}\")}\"\n    end\n\n    def verify(secret, body:, header:, tolerance: TOLERANCE)\n      ts, sig = header.to_s.split('.', 2)\n      ts = Integer(ts, exception: false)\n\n      raise InvalidSignatureError unless ts && sig\n\n      now = Time.current.to_i\n\n      raise TimestampError, 'Too old' if ts < now - tolerance\n      raise TimestampError, 'In future' if ts > now + tolerance\n\n      expected = OpenSSL::HMAC.hexdigest('sha256', secret, \"#{ts}.#{body}\")\n\n      raise InvalidSignatureError unless ActiveSupport::SecurityUtils.secure_compare(expected, sig)\n\n      true\n    end\n  end\nend\n","sourceCodeStart":8,"sourceCodeEnd":41,"githubUrl":"https://github.com/docusealco/docuseal/blob/004a22c1c88109c7ba0b567df011a8cb13894001/lib/webhook_urls/signatures.rb#L8-L41","documentation":"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.)","triggerScenarios":"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.","commonSituations":"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.","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/"],"exampleFix":"# before\nheader = OpenSSL::HMAC.hexdigest('sha256', secret, body) # digest only -> InvalidSignatureError\n# after\nheader = WebhookUrls::Signatures.sign(secret, body: body) # '1737561600.5f3a...' (ts.hmac)","handlingStrategy":"try-catch","validationCode":"def plausible_signature_header?(header)\n  header.to_s.match?(/\\A\\d+\\.[0-9a-f]{64}\\z/)\nend\nreturn head :unauthorized unless plausible_signature_header?(request.headers['Webhook-Signature'])","typeGuard":"def signed_header?(header)\n  !header.to_s.match(/\\A(\\d+)\\.([0-9a-f]{64})\\z/).nil?\nend","tryCatchPattern":"begin\n  WebhookUrls::Signatures.verify(secret, body: raw_request_body, header: request.headers['Webhook-Signature'])\nrescue WebhookUrls::Signatures::InvalidSignatureError\n  head :unauthorized # reject: malformed or mismatching signature; never process the payload\nend","preventionTips":["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"],"tags":["ruby","rails","webhook","hmac","security","signature"],"backgroundTag":"webhook-signature-verification-failed","analyzedSha":"004a22c1c88109c7ba0b567df011a8cb13894001","analyzedAt":"2026-08-21T13:38:23.343Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}