Проверка Hmac с помощью фляги в Python (со ссылкой на PHP и RUBY) - PullRequest
5 голосов
/ 23 июня 2019

Привет, ребята. Я работал над способом реализации проверки HMAC в python с помощью flask для веб-сайта selly.gg merchant.

Итак, в документации по selly для разработчиков приведены следующие примеры для проверки подписей HMAC (в PHP).и рубин): ​​https://developer.selly.gg/?php#signing-validating (код ниже:)

PHP:

<?php
        $signature = hash_hmac('sha512', json_encode($_POST), $secret);
        if hash_equals($signature, $signatureFromHeader) {
            // Webhook is valid 
        }
?>

RUBY:

signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha512'), secret, payload.to_json)
is_valid_signature = ActiveSupport::SecurityUtils.secure_compare(request.headers['X-Selly-Signature'], signature)

Итак, насколько я могВыясните: они не кодируют с помощью base64 (как shopify и другие), он использует SHA-512, кодирует секретный код вместе с данными ответов json и, наконец, заголовок запроса - «X-Selly-Signature»

Пока что я сделал следующий код (основываясь на коде shopify для подписи HMAC https://help.shopify.com/en/api/getting-started/webhooks):

SECRET = "secretkeyhere"
def verify_webhook(data, hmac_header):
    digest = hmac.new(bytes(SECRET, 'ascii'), bytes(json.dumps(data), 'utf8'), hashlib.sha512).hexdigest()
    return hmac.compare_digest(digest, hmac_header)
try:
    responsebody = request.json #line:22
    status = responsebody['status']#line:25
except Exception as e:
    print(e)
    return not_found()
print("X Selly sign: " + request.headers.get('X-Selly-Signature'))
verified = verify_webhook(responsebody, request.headers.get('X-Selly-Signature'))
print(verified)

Тем не менее, у него есть симулятор webhook, и даже с правильным секретным ключом и действительными запросами,verify_webhook всегда будет возвращать False. Я пытался связаться со службой поддержки Selly, но они не могли помочь мне больше, чем

Вы можете протестировать симулятор webhook по следующему адресу: https://selly.io/dashboard/{your account} / developer /webhook /симулировать

1 Ответ

3 голосов
/ 26 июня 2019

Вы почти правы, за исключением того, что вам не нужно json.dumps данные запроса.Скорее всего, это приведет к изменениям в выводе, таким как изменения в форматировании, которые не будут соответствовать исходным данным, что означает отказ HMAC.

Например,

{"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"}

отличается от:

{
  "id":"fd87d909-fbfc-466c-964a-5478d5bc066a"
}

, что на самом деле:

{x0ax20x20"id":"fd87d909-fbfc-466c-964a-5478d5bc066a"x0a}

Хеш будет полностью отличаться для двух входов.

Посмотрите, как json.loads и json.dumps изменит форматированиеи, следовательно, хэш:

http_data = b'''{
    "id":"fd87d909-fbfc-466c-964a-5478d5bc066a"
}
'''
print(http_data)
h = hashlib.sha512(http_data).hexdigest()
print(h)
py_dict = json.loads(http_data) # deserialise to Python dict
py_str = json.dumps(py_dict) # serialise to a Python str
py_bytes = json.dumps(py_dict).encode('utf-8') # encode to UTF-8 bytes
print(py_str)
h2 = hashlib.sha512(py_bytes).hexdigest()
print(h2)

Вывод:

b'{\n    "id":"fd87d909-fbfc-466c-964a-5478d5bc066a"\n}\n'
364325098....
{"id": "fd87d909-fbfc-466c-964a-5478d5bc066a"}
9664f687a....

Это не помогает, что пример PHP Селли показывает нечто подобное.На самом деле пример Selly PHP бесполезен, поскольку данные все равно не будут закодированы в форме, поэтому данные не будут в $_POST!

Вот мой маленький пример Flask:

import hmac
import hashlib
from flask import Flask, request, Response

app = Flask(__name__)

php_hash = "01e5335ed340ef3f211903f6c8b0e4ae34c585664da51066137a2a8aa02c2b90ca13da28622aa3948b9734eff65b13a099dd69f49203bc2d7ae60ebee9f5d858"
secret = "1234ABC".encode("ascii") # returns a byte object

@app.route("/", methods=['POST', 'GET'])
def selly():
    request_data = request.data # returns a byte object
    hm = hmac.new(secret, request_data, hashlib.sha512)
    sig = hm.hexdigest()

    resp = f"""req: {request_data}
    sig: {sig}
    match: {sig==php_hash}"""

    return Response(resp, mimetype='text/plain')

app.run(debug=True)

Обратите внимание на использование request.data для получения необработанного байтового ввода и простое использование encode на secret str для получения закодированных байтов (вместо использования подробного экземпляра bytes()).

Это можно проверить с помощью:

curl -X "POST" "http://localhost:5000/" \
 -H 'Content-Type: text/plain; charset=utf-8' \
 -d "{\"id\":\"fd87d909-fbfc-466c-964a-5478d5bc066a\"}"

Я также создал немного PHP для проверки того, что оба языка создают одинаковый результат:

<?php
    header('Content-Type: text/plain');
    $post = file_get_contents('php://input');
    print $post;
    $signature = hash_hmac('sha512', $post, "1234ABC");
    print $signature;
?>
...