Search code examples
slacknim-lang

Nim Slack bot Signature Verification Issues


I'm fairly new to Nim, and I suspect I'm just doing something wrong here. I'm using Jester (for routing, etc) and Nimcrytpo (for hmac) but something isn't adding up. Here's how I'm attempting to verify a signature:

import jester
import dotenv
import os, strutils, times
import nimcrypto

const timestampHeader = "X-Slack-Request-Timestamp"
const slackSignatureHeader = "X-Slack-Signature"
const signatureVersion = "v0"
const signingSecret = os.getEnv("SLACK_SIGNING_SECRET")

proc isTimestampRecent(timestamp: int): bool =
  abs(getTime().toUnix - timestamp) <= (60 * 5)

proc verifySignature*(request: Request): bool =
  if (not request.headers.hasKey timestampHeader) or
  (not request.headers.hasKey slackSignatureHeader):
    return false

  let timestamp = request.headers[timestampHeader].parseInt
  if not timestamp.isTimestampRecent():
    return false

  let baseString = signatureVersion & ':' & $timestamp & ':' & $request.body
  let mySignature = sha256.hmac(signingSecret, baseString)

  let slackSignature = MDigest[256].fromHex(request.headers[slackSignatureHeader])

  mySignature == slackSignature

A few things I'm running into:

  1. The signature doesn't match, and I'm not really sure how to debug that. I'm definitely getting a valid request from Slack and following the instructions for verification here: https://api.slack.com/authentication/verifying-requests-from-slack#about, but it's incorrect.
  2. I know I'm missing the v0= in the comparison, but I'm not quite sure how to do that with the time independent comparison (whether I should be skipping that part or not in the comparison, etc)

My best guess at this point is that somehow the Jester/Httpbeast request body isn't "raw" enough (though it's just plain json...?) or is somehow processed.

Any help or suggestions on how to debug would be greatly appreciated. Thank you in advance!


Solution

  • After fussing with this for a while, I found I was doing a number of things incorrectly! Hopefully this helps others:

    1. The signingSecret is pulled from the env, so it shouldn't be a constant--I moved that into the proc itself defined with let instead.
    2. The slack signature in the headers is prefixed with v0=, which makes it the wrong length for MDigest[256].fromHex(), so that was ending up as the null value (0000...) instead of what should've been.

    Here's a working version, now in case anyone else should need one. Please let me know if you see anything that could be improved as well.

    import jester
    import dotenv
    import os, strutils, times
    import nimcrypto
    
    const timestampHeader = "X-Slack-Request-Timestamp"
    const slackSignatureHeader = "X-Slack-Signature"
    const signatureVersion = "v0"
    
    proc isTimestampRecent(timestamp: int): bool =
      abs(getTime().toUnix - timestamp) <= (60 * 5)
    
    proc verifySignature*(request: Request): bool =
      let signingSecret = os.getEnv("SLACK_SIGNING_SECRET")
    
      if (not request.headers.hasKey timestampHeader) or
      (not request.headers.hasKey slackSignatureHeader):
        return false
    
      let timestamp = request.headers[timestampHeader].parseInt
      if not timestamp.isTimestampRecent():
        return false
    
      let baseString = signatureVersion & ':' & $timestamp & ':' & $request.body
      let mySignature = sha256.hmac(signingSecret, baseString)
    
      var rawSlackSignature: string = $request.headers[slackSignatureHeader]
      rawSlackSignature.removePrefix(signatureVersion & '=')
    
      let slackSignature = MDigest[256].fromHex(rawSlackSignature)
    
      mySignature == slackSignature