在 PHP 中从 Microsoft Teams 自定义机器人验证 HMAC

Verifying HMAC from Microsoft Teams custom Bot in PHP

我正在尝试使用 PHP 对 Microsoft Teams 自定义机器人进行身份验证,遵循 Microsoft instructions 并阅读 de C# 示例代码。

Microsoft Intructions steps:
1. Generate the hmac from the request body of the message. There are standard libraries on most platforms. Microsoft Teams uses standard SHA256 HMAC cryptography. You will need to convert the body to a byte array in UTF8.
2. To compute the hash, provide the byte array of the shared secret.
3. Convert the hash to a string using UTF8 encoding.
4. Compare the string value of the generated hash with the value provided in the HTTP request.

我写了一个小 php 脚本来在本地测试它:

        <?php
        //Function to generate C# byte[] equivalent
        function unpak_str($val){
            $b = unpack('C*', $val);
            foreach ($b as $key => $value)
                $byte_a .= $value;

           return $byte_a;
          }

        //multi test outputs
        function hasher($values=[], &$output){
            //my secret share
            $secret="ejWiKHgsKY1ZfpJwJ+wIiN4+bgsFad/lkpu9/MWNXgM=";
            //diferent test
            $secret_64=base64_decode($secret);
            $secret_b=unpak_str($secret);
            $secret_b_64=unpak_str(base64_decode($secret));

            foreach($values as $msg){
                $hs = hash_hmac("sha256",$msg,$secret, true);
                $hs_64 = hash_hmac("sha256",$msg,$secret_64, true);
                $hs_b = hash_hmac("sha256",$msg,$secret_b, true);
                $hs_b_64 = hash_hmac("sha256",$msg,$secret_b_64, true);

                $output.=base64_encode($hs)." <BR>";
                $output.=base64_encode($hs_64)." <BR>";
                $output.=base64_encode($hs_b)." <BR>";
                $output.=base64_encode($hs_b_64)." <BR>";
             }
          }

    //Get data
    $data=file_get_contents('php://input');

    //real data request content for test
    $data ='{type":"message","id":"1512376018086","timestamp":"2017-12-04T08:26:58.237Z","localTimestamp":"2017-12-04T09:26:58.237+01:00","serviceUrl":"https://smba.trafficmanager.net/emea-client-ss.msg/","channelId":"msteams","from":{"id":"29:1aq6GCrC6lM9dv3YkAYi1gxTPiLnojGFgVr0_Th-2x6DhqmHAOhFwQHFzSyDy5RruXY4_FZjJebKHU7bpxfHpXA","name":"ROBERTO ALONSO FERNANDEZ","aadObjectId":"1e0dc7a0-9d5e-488b-bcf2-7e39c84076b8"},"conversation":{"isGroup":true,"id":"19:9e1c52275dfb4d0b873ddf34eb9f4979@thread.skype;messageid=1512376018086","name":null},"recipient":null,"textFormat":"plain","attachmentLayout":null,"membersAdded":[],"membersRemoved":[],"topicName":null,"historyDisclosed":null,"locale":null,"text":"<at>PandoBot</at> fff","speak":null,"inputHint":null,"summary":null,"suggestedActions":null,"attachments":[{"contentType":"text/html","contentUrl":null,"content":"<div><span itemscope=\"\" itemtype=\"http://schema.skype.com/Mention\" itemid=\"0\">PandoBot</span> fff</div>","name":null,"thumbnailUrl":null}],"entities":[{"type":"clientInfo","locale":"es-ES","country":"ES","platform":"iOS"}],"channelData":{"teamsChannelId":"19:9e1c52275dfb4d0b873ddf34eb9f4979@thread.skype","teamsTeamId":"19:1e04f564ce5e4596bf2f266dbcff439e@thread.skype","channel":{"id":"19:9e1c52275dfb4d0b873ddf34eb9f4979@thread.skype"},"team":{"id":"19:1e04f564ce5e4596bf2f266dbcff439e@thread.skype"},"tenant":{"id":"9744600e-3e04-492e-baa1-25ec245c6f10"}},"action":null,"replyToId":null,"value":null,"name":null,"relatesTo":null,"code":null}';


    //generate HMAC hash with diferent $data formats
    $test = [$data, unpak_str($data), base64_encode($data), unpak_str(base64_encode($data))];
    hasher($test, $output);


    //microsoft provided HMAC
    $output.="<HR>EW2993goL1q7nGhytIb3jKmV6luXLz15Bq2aYwuCeiE="; 


    echo $output;
    /*
    Calculates: 
    0HsKoHza/QBvdz+nZw9tOti/eSWjyMMt/U77bfDqiE8=
    3jSq3I0HNQkjB9QfnnsxC1c3pF5PjqweHlSVcicrShY=
    bTQcGVTHX8/Gh4xovnN0WiJUiNaOQwvUZnwyFfiCaJE=
    qHBT2Y2ITyoxz2gmBbG8P1CrClvETus6dTffET3bAR8=
    8BcrXEQDDi77qgxCZLYyb/6ez8p9Qg2ZhTyZPWkdn/g=
    +8RSU5SSJKxqRLKkI+NkTE01xwu6PwPkKKMuvyyUvlo=
    PdL5ZpEwcN6Fe5kfX7zeAZLJvt0uLNTzu7lhuoOcr2o=
    s6M5pYruEgWeNMEOFfQRjVKQqtPBVaW3TJb2MzObF2c=
    xOTLhddbAwczQVneuTDQhPzmoIXGQljpf27c+hlhQII=
    aUMm5b2sKfmwGZOglfiu228fWqoLlwjc7z1QRdIbakE=
    5a7bAj9tzqhP9l85OvfVasURW0GSV5rykRutFFPO2fk=
    kwg6P2LoDL9rc3SSwJxQeoYJzZYlh+FHFefe38UokBM=
    eHeAzI7TV6vYDzxTxwyKWxMeVKFiFlIffWRiIMAk6fk=
    ZCyj2UppacQOTXogLPMFLDeMArQg03rhhlIwhynDvng=
    uQYK+7u9fppb62zXqtVYfkNK9wVawB3g+BlTyu4dc74=
    vjOFA3fqpwUx/VO9dQv3XviNhpjTNQsUwaJIwH4JjdY=
    ------------ MS PROVIDED HMAC ---------------
    EW2993goL1q7nGhytIb3jKmV6luXLz15Bq2aYwuCeiE=
     */

我的哈希匹配为零...

您不需要 unpack(),或者那个 unpak_str() 函数(它也被破坏了,因为它只是用下一个字节覆盖每个字节,而不是追加它们)。

PHP 中没有字节数组 - 该语言没有不同的字符串类型;如何解释字符串完全取决于使用它们的函数。也就是说,您的共享秘密应该只是 base64_encode($secret).

的结果

我不是 PHP 专家,你涵盖所有情况的逻辑有点复杂,但我很确定你的问题是你没有转换消息 ($data) ) 在计算 HMAC 之前来自 UTF8。

这是 Node 中的一个简单的自定义 echo bot,展示了如何计算和验证 HMAC:

const util = require('util');
const crypto = require('crypto');
const sharedSecret = "+ZaRRMC8+mpnfGaGsBOmkIFt98bttL5YQRq3p2tXgcE=";
const bufSecret = Buffer(sharedSecret, "base64");

var http = require('http');
var PORT = process.env.port || process.env.PORT || 8080;

http.createServer(function(request, response) { 
    var payload = '';
    request.on('data', function (data) {
        // console.log("Chunk size: %s bytes", data.length)
        payload += data;
    });

    request.on('end', function() {
        try {
            // Retrieve authorization HMAC information
            var auth = this.headers['authorization'];
            // Calculate HMAC on the message we've received using the shared secret         
            var msgBuf = Buffer.from(payload, 'utf8');
            var msgHash = "HMAC " + crypto.createHmac('sha256', bufSecret).update(msgBuf).digest("base64");
            console.log("Computed HMAC: " + msgHash);
            console.log("Received HMAC: " + auth);

            response.writeHead(200);
            if (msgHash === auth) {
                var receivedMsg = JSON.parse(payload);
                var responseMsg = '{ "type": "message", "text": "You typed: ' + receivedMsg.text + '" }';   
            } else {
                var responseMsg = '{ "type": "message", "text": "Error: message sender cannot be authenticated." }';
            }
            response.write(responseMsg);
            response.end();
        }
        catch (err) {
            response.writeHead(400);
            return response.end("Error: " + err + "\n" + err.stack);
        }
    });

}).listen(PORT);

console.log('Listening on port %s', PORT);

最后经过大量的尝试,这让我发疯了,决定用一个新的秘密开始一个新的机器人。现在工作正常。我是人类,而 MS Teams 没有...我想这是我对 copy/paste 的错,但这是一件非常奇怪的事情,另一方面,旧机器人失败了很多次,没有响应,最新的没有

PHP 中 Microsoft Teams 自定义机器人的完整示例验证 HMAC:

        <?php

        //The secret share with Microsoft Teams
        $secret="jond3021g9imMkrt8txF5AVPIwPFouNV/I72cQFii18=";

        //get headers
        $a = getallheaders();
        $provided_hmac=substr($a['Authorization'],5);

        //Get data from request
        $data=file_get_contents('php://input');

        //json decode into array
        $json=json_decode($data, true);

        //hashing
        $hash = hash_hmac("sha256",$data,base64_decode($secret), true);
        $calculated_hmac = base64_encode($hash);

        //start log var
        $log = "\n========".date("Y-m-d H:i:s")."========\n".$provided_hmac."\n".$calculated_hmac."\n";

        try{
            //compare hashs
            if(!hash_equals($provided_hmac,$calculated_hmac))
                throw new Exception("No hash matching");
            //response text
            $txt="Hi {$json["from"]["name"]} welcome to your custom bot";
            echo '{
                "type": "message",
                "text": "'.$txt.'"
                 }';
            $log .= "Sended: {$txt}";
        }catch (Exception $e){
            $log .= $e->getMessage();
        }
        //write log
        $fp = fopen("log.txt","a");
        fwrite($fp, $log . PHP_EOL);
        fclose($fp);