草庐IT

java - 松弛请求验证 : Can't compute matching request digest using signed secret

coder 2024-03-30 原文

我正在 Slack 上实现交互式消息,其中包含一些操作按钮。使用 Slack App 我能够处理 Slack 用户点击我的 Java Springboot API 上的按钮。

到这一刻,一切都很好。但是,我努力计算匹配的请求签名(摘要)以验证它实际上来自 Slack。我阅读了 Slack verification documentation page 上的所有文档。

该页面描述,签名必须计算为 HMAC SHA256 哈希,使用 Signing Secret 作为 key 和内容作为松弛版本、时间戳和请求主体的串联,例如:

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

页面上写着:

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

...所以我没有在哈希计算之前对请求进行编码/反序列化(我在下面附上了我从 Slack 收到的请求)

为了计算哈希,我使用在 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);
    }
}

我也尝试了这个 online hash generator 来比较结果,它们是一样的。

从 Slack 收到的请求如下所示:

{
    "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.

我也尝试从 URL 解码主体计算哈希值,但仍然无法获得匹配的签名。

我做错了什么吗?感谢您的回答/提示。


编辑:这是我的 REST Controller 和请求 validator 的完整源代码:

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";
    }
}

最佳答案

我在 Node.js 实现中偶然发现了同样的问题,并找到了这个 Medium article声明如下:

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.

这两种编码有什么区别?空格的解析方式:

  • RFC3986 会将 "" 转换为 "%20"
  • RFC1738 会将 "" 转换为 "+"

对于 Node.js,建议安装 qs并像这样使用它:

qs.stringify(req.body, { format : 'RFC1738' });

关于java - 松弛请求验证 : Can't compute matching request digest using signed secret,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51264984/

有关java - 松弛请求验证 : Can't compute matching request digest using signed secret的更多相关文章

  1. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  2. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  3. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  4. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  5. ruby-on-rails - 如果为空或不验证数值,则使属性默认为 0 - 2

    我希望我的UserPrice模型的属性在它们为空或不验证数值时默认为0。这些属性是tax_rate、shipping_cost和price。classCreateUserPrices8,:scale=>2t.decimal:tax_rate,:precision=>8,:scale=>2t.decimal:shipping_cost,:precision=>8,:scale=>2endendend起初,我将所有3列的:default=>0放在表格中,但我不想要这样,因为它已经填充了字段,我想使用占位符。这是我的UserPrice模型:classUserPrice回答before_val

  6. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  7. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  8. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  9. ruby - 主要 :Object when running build from sublime 的未定义方法 `require_relative' - 2

    我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby​​1.9+ 关于ruby-主要:Objectwhenrun

  10. ruby - 无法让 RSpec 工作—— 'require' : cannot load such file - 2

    我花了三天的时间用头撞墙,试图弄清楚为什么简单的“rake”不能通过我的规范文件。如果您遇到这种情况:任何文件夹路径中都不要有空格!。严重地。事实上,从现在开始,您命名的任何内容都没有空格。这是我的控制台输出:(在/Users/*****/Desktop/LearningRuby/learn_ruby)$rake/Users/*******/Desktop/LearningRuby/learn_ruby/00_hello/hello_spec.rb:116:in`require':cannotloadsuchfile--hello(LoadError) 最佳

随机推荐