Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Slack request verification: Can't compute matching request digest using signed secret

I'm implementing interactive messages on Slack, which contains some action buttons. Using Slack App I'm able to handle Slack users clicking the buttons on my Java Springboot API.

To this moment, everything is fine. However, I struggle to compute matching request signature (digest) to verify, that it actually comes from Slack. I read all the documentation for that on Slack verification documentation page.

The page decribes, that the signature has to be computed as a HMAC SHA256 hash, using Signing Secret as a key and content as concatenation of slack version, timestamp and request body, for example:

v0:123456789:command=/weather&text=94070

On the page is stated:

...Evaluate only the raw HTTP request body when computing signatures.

... so I'm not encoding/deserializing the request before hash computing (I've attached my received request from Slack below)

To compute the hash I use the code found on StackOverflow:

private String computeMessageDigest(String content) {
    final String ALGORITHM = "HmacSHA256";
    final String UTF_8 = "UTF-8";

    try {
        Key signingKey = new SecretKeySpec(signingSecret.getBytes(UTF_8), ALGORITHM);
        Mac mac = Mac.getInstance(ALGORITHM);
        mac.init(signingKey);

        return Hex.encodeHexString(mac.doFinal(content.getBytes(UTF_8)));
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

I tried also this online hash generator to compare the results, and they were the same.

The request received from Slack looks like this:

{
    "headers": {
        "x-forwarded-for": ["::ffff:52.72.111.29"],
        "x-forwarded-proto": ["https"],
        "x-pagekite-port": ["443"],
        "host": ["inqool.pagekite.me"],
        "user-agent": ["Slackbot 1.0 (+https://api.slack.com/robots)"],
        "accept-encoding": ["gzip,deflate"],
        "accept": ["application/json,*/*"],
        "x-slack-signature": ["v0=87fbffb089501ba823991cc20058df525767a8a2287b3809f9afff3e3b600dd8"],
        "x-slack-request-timestamp": ["1531221943"],
        "content-length": ["2731"],
        "Content-Type": ["application/x-www-form-urlencoded;charset=UTF-8"]
    },
    "body": "payload=%7B%22type%22%3A%22interactive_message%22%2C%22actions%22%3A%5B%7B%22name%22%3A%22reject_btn%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22false%22%7D%5D%2C%22callback_id%22%3A%22artwork%3D40d7a87f-466c-4fc9-b454-09ce020d4465%22%2C%22team%22%3A%7B%22id%22%3A%22T03NP6SA7%22%2C%22domain%22%3A%22artstaq%22%7D%2C%22channel%22%3A%7B%22id%22%3A%22G8F2WR4FJ%22%2C%22name%22%3A%22privategroup%22%7D%2C%22user%22%3A%7B%22id%22%3A%22U66T9QX60%22%2C%22name%22%3A%22majo%22%7D%2C%22action_ts%22%3A%221531221943.512498%22%2C%22message_ts%22%3A%221531221198.000225%22%2C%22attachment_id%22%3A%221%22%2C%22token%22%3A%22ZABrZDXgJCOOLNau5mXnfNQR%22%2C%22is_app_unfurl%22%3Afalse%2C%22original_message%22%3A%7B%22text%22%3A%22User+just+put+item+on+*EXCHANGE*.%22%2C%22bot_id%22%3A%22BBM1W4QEL%22%2C%22attachments%22%3A%5B%7B%22author_name%22%3A%22Slack+Test%3B+slack%40test.com%22%2C%22callback_id%22%3A%22artwork%3D40d7a87f-466c-4fc9-b454-09ce020d4465%22%2C%22fallback%22%3A%22Slack+Test%3B+%3Cmailto%3Aslack%40test.com%7Cslack%40test.com%3E+just+put+item+Panenka+%5C%2F+Doll+by+artist+Jaroslav+Vale%5Cu010dka+into+ON+REQUEST+mode%22%2C%22text%22%3A%22%3Chttp%3A%5C%2F%5C%2Flocalhost%3A8080%5C%2Fartist%5C%2F609cd328-d533-4ab0-b982-ec2f104476f2%7CJaroslav+Vale%5Cu010dka%3E%22%2C%22title%22%3A%22Panenka+%5C%2F+Doll%22%2C%22footer%22%3A%22ARTSTAQ+Slack+Reporter%22%2C%22id%22%3A1%2C%22title_link%22%3A%22http%3A%5C%2F%5C%2Flocalhost%3A8080%5C%2Fartwork%5C%2F40d7a87f-466c-4fc9-b454-09ce020d4465%22%2C%22color%22%3A%22f0d0ad%22%2C%22fields%22%3A%5B%7B%22title%22%3A%22Trading+type%22%2C%22value%22%3A%22ON+REQUEST%22%2C%22short%22%3Atrue%7D%5D%2C%22actions%22%3A%5B%7B%22id%22%3A%221%22%2C%22name%22%3A%22approve_btn%22%2C%22text%22%3A%22APPROVE%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22true%22%2C%22style%22%3A%22primary%22%2C%22confirm%22%3A%7B%22text%22%3A%22Do+you+really+want+to+approve+this+artwork%3F%22%2C%22title%22%3A%22Approve+artwork%22%2C%22ok_text%22%3A%22Yes%22%2C%22dismiss_text%22%3A%22Cancel%22%7D%7D%2C%7B%22id%22%3A%222%22%2C%22name%22%3A%22reject_btn%22%2C%22text%22%3A%22REJECT%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22false%22%2C%22style%22%3A%22danger%22%2C%22confirm%22%3A%7B%22text%22%3A%22Do+you+really+want+to+reject+this+artwork%3F%22%2C%22title%22%3A%22Reject+artwork%22%2C%22ok_text%22%3A%22Yes%22%2C%22dismiss_text%22%3A%22Cancel%22%7D%7D%5D%7D%5D%2C%22type%22%3A%22message%22%2C%22subtype%22%3A%22bot_message%22%2C%22ts%22%3A%221531221198.000225%22%7D%2C%22response_url%22%3A%22https%3A%5C%2F%5C%2Fhooks.slack.com%5C%2Factions%5C%2FT03NP6SA7%5C%2F395760858899%5C%2FGlP9jsNQak7FqEciEHhscx4L%22%2C%22trigger_id%22%3A%22395632563524.3771230347.851ab60578de033398338a9faeb41a15%22%7D"
}

When I computed the HMAC SHA256 hash, I got 561034bb6860c07a6b4eaf245b6da3ea869c7806c7f7be20b1a830b6d25c54c8 but I should get 87fbffb089501ba823991cc20058df525767a8a2287b3809f9afff3e3b600dd8, as in the request header.

I also tried to compute the hash from the URL decoded body, but still not be able to get the matching signature.

Am I doing something wrong? Thanks for the answers/hints.


EDIT: here's the whole source code of my REST controller and request verifier:

package com.artstaq.resource;

import com.artstaq.integration.slack.SlackRequestVerifier;
import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.inject.Inject;

@RestController
@RequestMapping("/content_admin")
public class ContentAdminResource {

    private SlackRequestVerifier slackVerifier;


    @RequestMapping(value = "/slack/artwork/resolve", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public void resolve(HttpEntity<String> request) {
        slackVerifier.verifySlackRequest(request);
    }


    @Inject
    public void setSlackVerifier(SlackRequestVerifier slackVerifier) {
        this.slackVerifier = slackVerifier;
    }
}

package com.artstaq.integration.slack;

import com.artstaq.exception.SignatureVerificationException;
import com.artstaq.exception.TimestampTooOldException;
import org.apache.commons.codec.binary.Hex;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Component;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

/**
 * Class providing request verification received from Slack
 */
@Component
public class SlackRequestVerifier {

    @Value("${integration.slack.version:v0}")
    private String version;

    @Value("${integration.slack.signingSecret}")
    private String signingSecret;

    /**
     * Verifies the integrity of received Slack request.
     */
    public void verifySlackRequest(HttpEntity<String> request) {
        String timestamp = request.getHeaders().getFirst(SlackHeaders.TIMESTAMP);
        Instant timeInstant = Instant.ofEpochSecond(Long.valueOf(timestamp));
        if (timeInstant.plus(5, ChronoUnit.MINUTES).compareTo(Instant.now()) < 0) {
            throw new TimestampTooOldException(timeInstant);
        }

        String expectedDigest = request.getHeaders().getFirst(SlackHeaders.SIGNATURE);

        String basestring = String.join(":", version, timestamp, request.getBody());
        String computedDigest = version + "=" + computeMessageDigest(basestring);

        if (!computedDigest.equals(expectedDigest)) {
            throw new SignatureVerificationException(expectedDigest, computedDigest);
        }
    }

    /**
     * Compute HMAC SHA256 digest for given content using defined slack signing secret
     */
    private String computeMessageDigest(String content) {
        final String ALGORITHM = "HmacSHA256";
        final String UTF_8 = "UTF-8";

        try {
            Key signingKey = new SecretKeySpec(signingSecret.getBytes(UTF_8), ALGORITHM);
            Mac mac = Mac.getInstance(ALGORITHM);
            mac.init(signingKey);

            return Hex.encodeHexString(mac.doFinal(content.getBytes(UTF_8)));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    private static class SlackHeaders {
        private static final String TIMESTAMP = "X-Slack-Request-Timestamp";
        private static final String SIGNATURE = "X-Slack-Signature";
    }
}
like image 880
Majo Avatar asked Jul 10 '18 12:07

Majo


3 Answers

I stumbled upon the very same problem on a Node.js implementation, and found this Medium article which states the following:

Note: We cannot use the built-in querystring Node package because it only supports RFC3986 space encoding and Slack requires us to implement RFC1738 space encoding.

What's the difference between both encodings? The way spaces are parsed:

  • RFC3986 will convert " " to "%20"
  • RFC1738 will convert " " to "+"

For Node.js, it suggests to install qs and to use it like this:

qs.stringify(req.body, { format : 'RFC1738' });
like image 174
Joel A. Villarreal Bertoldi Avatar answered Nov 15 '22 17:11

Joel A. Villarreal Bertoldi


The following worked for us:

public enum SigningVerification {
    VERIFIED,
    DENIED
}

public SigningVerification verify(ImmutableSigningSecretRequest request) {
    String basestring = String.join(":", "v0", request.timestamp(), request.body());
    SecretKeySpec secret_key = new SecretKeySpec(signingSecret.getBytes(), "HmacSHA256");
    Mac sha256_HMAC = Try.of(() -> Mac.getInstance("HmacSHA256")).getOrElseThrow((SupplierRuntimeException) RuntimeException::new);
    Try.run(() -> sha256_HMAC.init(secret_key));
    String hash = "v0=" + Hex.encodeHexString(sha256_HMAC.doFinal(basestring.getBytes()));
    return hash.equals(request.verificationSignature()) ? VERIFIED : DENIED;
}

Controller:

@PostMapping("/command")
public RichMessage postCommand(@RequestHeader(value = "X-Slack-Request-Timestamp") String timestamp,
                               @RequestHeader(value = "X-Slack-Signature") String signature,
                               @RequestParam(value = "text", required = false) String message,
                               @RequestBody String body) {
    SigningSecretVerification.SigningVerification verification = verifier.verify(ImmutableSigningSecretRequest
            .builder()
            .timestamp(timestamp)
            .verificationSignature(signature)
            .body(body)
            .build()
    );
    return new RichMessage(message);

}

We basically just followed the steps in the Slack doc and it works fine.

like image 20
Stefan Avatar answered Nov 15 '22 16:11

Stefan


I had the same problem, using Spring's @RequestBody.

After going to the trouble of setting up a mitmproxy between Slack and my Spring app in order to compare request bodies, it turned out that Spring was decoding e.g. asterisk characters instead of leaving them as %2A.

The fix for me was to switch to asking for a [HttpServletRequest](https://github.com/boclips/terry/commit/c51382a5a6a9e96d5b19e22b038654bfb19b65b0#diff-79f3c274c9fa96261f8c9e09306a088bR37) (doesn't need a Spring annotation) and reading the raw body from it using `request.reader.use { it.readText() }` (using Kotlin's `use` to close the reader object after reading).

EDIT: the above technique doesn't work, and getting the raw request body from Spring is a mission in itself! Ongoing.

like image 3
Andrew Bruce Avatar answered Nov 15 '22 15:11

Andrew Bruce