使用 Python RSA 将请求复制到 Chef

Replicating request to Chef with Python RSA

目标:我需要一个 Python 3 的 Chef's REST API 包装器。因为它是 Python-3,所以 PyChef 是不可能的。

问题:我正在尝试使用 Python RSA 复制 Chef 请求。但是包装器会导致错误消息:"Invalid signature for user or client 'XXX'".

我通过尝试复制 Chef Authentication and Authorization with cURL using a Python RSA package: RSA Signing and verification 中显示的 cURL 脚本来接近包装器。

这是我重写的。它可能更简单,但我开始对换行符和 headers 顺序感到疑惑,所以添加了一些不必要的东西:

import base64
import hashlib
import datetime
import rsa
import requests
import os
from collections import OrderedDict

body = ""
path = "/nodes"
client_name = "anton"
client_key = "/Users/velvetbaldmime/.chef/anton.pem"
# client_pub_key = "/Users/velvetbaldmime/.chef/anton.pub"

hashed_body = base64.b64encode(hashlib.sha1(body.encode()).digest()).decode("ASCII")
hashed_path = base64.b64encode(hashlib.sha1(path.encode()).digest()).decode("ASCII")
timestamp = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
canonical_request = 'Method:GET\nHashed Path:{hashed_path}\nX-Ops-Content-Hash:{hashed_body}\nX-Ops-Timestamp:{timestamp}\nX-Ops-UserId:{client_name}'
canonical_request = canonical_request.format(
    hashed_body=hashed_body, hashed_path=hashed_path, timestamp=timestamp, client_name=client_name)
headers = "X-Ops-Timestamp:{timestamp}\nX-Ops-Userid:{client_name}\nX-Chef-Version:0.10.4\nAccept:application/json\nX-Ops-Content-Hash:{hashed_body}\nX-Ops-Sign:version=1.0"
headers = headers.format(
    hashed_body=hashed_body, hashed_path=hashed_path, timestamp=timestamp, client_name=client_name)

headers = OrderedDict((a.split(":", 2)[0], a.split(":", 2)[1]) for a in headers.split("\n"))

headers["X-Ops-Timestamp"] = timestamp

with open(client_key, 'rb') as privatefile:
    keydata = privatefile.read()
    privkey = rsa.PrivateKey.load_pkcs1(keydata)

with open("pubkey.pem", 'rb') as pubfile:
    keydata = pubfile.read()
    pubkey = rsa.PublicKey.load_pkcs1_openssl_pem(keydata)

signed_request = base64.b64encode(rsa.sign(canonical_request.encode(), privkey, "SHA-1"))
dummy_sign = base64.b64encode(rsa.sign("hello".encode(), privkey, "SHA-1"))

print(dummy_sign)

def chunks(l, n):
    n = max(1, n)
    return [l[i:i + n] for i in range(0, len(l), n)]

auth_headers = OrderedDict(("X-Ops-Authorization-{0}".format(i+1), chunk) for i, chunk in enumerate(chunks(signed_request, 60)))

all_headers = OrderedDict(headers)
all_headers.update(auth_headers)

# print('curl '+' \\n'.join("-H {0}: {1}".format(i[0], i[1]) for i in all_headers.items())+" \\nhttps://chef.local/nodes")

print(requests.get("https://chef.local"+path, headers=all_headers).text)

在每一步我都尝试检查变量是否与 curl 脚本中的对应变量具有相同的结果。

问题似乎出在签名阶段 - python 的软件包输出与我的 mac 的 openssl 工具之间存在明显差异。由于这种差异,厨师 returns {"error":["Invalid signature for user or client 'anton'"]}。具有相同值和键的卷曲脚本工作正常。

dummy_sign = base64.b64encode(rsa.sign("hello".encode(), privkey, "SHA-1")) 来自 Python 的值为

N7QSZRD495vV9cC35vQsDyxfOvbMN3TcnU78in911R54IwhzPUKnJTdFZ4D/KpzyTVmVBPoR4nY5um9QVcihhqTJQKy+oPF+8w61HyR7YyXZRqmx6sjiJRffC4uOGb5Wjot8csAuRSeUuHaNTl6HCcfRKnwUZnB7SctKoK6fXv0skWN2CzV9CjfHByct3oiy/xAdTz6IB+fLIwSQUf1k7lJ4/CmLJLP/Gu/qALkvWOYDAKxmavv3vYX/kNhzApKgTYPMw6l5k1aDJGRVm9Ch/BNQbg1WfZiT6LK+m4KAMFbTORfEH45KGWBCj9zsyETyMCAtUycebjqMujMqEwzv7w==

echo -n "hello" | openssl rsautl -sign -inkey ~/.chef/anton.pem | openssl enc -base64 的输出是

WfoASF1f5DPT3CVPlWDrIiTwuEnjr5yCV+WIlbQLFmwm3nfhIqfTPLyTM56SwTSg
CKdboVU4EBFxC3RsU2aPpELqRH6+Fnl2Tl273vo6kLzvC/8+tUBTdNZdzSPhx6S8
x+6wzVFXsd3QeGAWoHkEgTKodSByFzARnZFxO2JzUe4dnygijwruHdf9S4ldrRo6
eaShwaxuNzM0cIl+Umz5iym3cCD6GFL13njmXZs3cHRLesBtLKA7pNxJ1UDf2WN2
OK09aK+bHaM4jl5HeQ2SdNzBQIKvyDcxX4Divnf2I/0tzD16J6BEMGCfTfsI2f3K
TVGulq81+sH9zo8lGnpDrw==

我无法在 rsautl 的 openssl 中找到有关默认哈希算法的信息,但我猜它是 SHA-1。

此时我真的不知道该怎样看,希望大家帮忙改正。

您可以自动化与 Chef 的交互 - 使用这些工具:

注:

正如 jww 评论的那样,OP 提到他不想使用 Selenium。
但是,我希望我的答案是完整的(除了 OP 之外,其他人(包括我)可能会使用它),我在列表中包含了 Selenium。

来自 Chef Authentication and Authorization with cURL

timestamp=$(date -u "+%Y-%m-%dT%H:%M:%SZ")  

时间是 UTC,所以在 Python 中,它必须是

timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")  

openssl 相当于 Python、

dummy_sign = base64.b64encode(rsa.sign("hello".encode(), privkey, "SHA-1"))  

echo -n hello|openssl dgst -sha1 -sign ~/.chef/anton.pem -keyform PEM|openssl enc -base64

在 Python 代码中,您正在对消息的消息摘要 SHA-1 进行签名。这就是所谓的分离签名。

echo -n "hello" | openssl rsautl -sign -inkey ~/.chef/anton.pem | openssl enc -base64 但是这个签名了整个消息,没有做摘要。

Python rsa 模块没有 openssl rsautl -sign 的等效项。所以我定义了一个函数来填充 space。

from rsa import common, transform, core, varblock
from rsa.pkcs1 import _pad_for_signing

def pure_sign(message, priv_key):
    '''Signs the message with the private key.

    :param message: the message to sign. Can be an 8-bit string or a file-like
        object. If ``message`` has a ``read()`` method, it is assumed to be a
        file-like object.
    :param priv_key: the :py:class:`rsa.PrivateKey` to sign with
    :return: a message signature block.
    :raise OverflowError: if the private key is too small to contain the
        requested hash.

    '''

    keylength = common.byte_size(priv_key.n)
    padded = _pad_for_signing(message, keylength)

    payload = transform.bytes2int(padded)
    encrypted = core.encrypt_int(payload, priv_key.d, priv_key.n)
    block = transform.int2bytes(encrypted, keylength)

    return block

测试;
openssl

echo -n hello|openssl rsautl -sign -inkey .chef/anton.pem |base64  

foIy6HVpfIpNk4hMYg8YWCEZwZ7w4Qexr6KXDbJ7/vr5Jym56joofkn1qUak57iSercqQ1xqBsIT
fo6bDs2suYUKu15nj3FRQ54+LcVKjDrUUEyl2kfJgVtXLsdhzYj1SBFJZnbz32irVMVytARWQusy
b2f2GQKLTogGhCywFFyhw5YpAHmKc2CQIHw+SsVngcPrmVAAtvCZQRNV5zR61ICipckNEXnya8/J
Ga34ntyELxWDradY74726OlJSgszpHbAOMK02C4yx7OU32GWlPlsZBUGAqS5Tu4MSjlD1f/eQBsF
x/pn8deP4yuR1294DTP7dsZ9ml64ZlcIlg==

Python

base64.b64encode(pure_sign.pure_sign(b'hello',prik)).decode()

'foIy6HVpfIpNk4hMYg8YWCEZwZ7w4Qexr6KXDbJ7/vr5Jym56joofkn1qUak57iSercqQ1xqBsITfo6bDs2suYUKu15nj3FRQ54+LcVKjDrUUEyl2kfJgVtXLsdhzYj1SBFJZnbz32irVMVytARWQusyb2f2GQKLTogGhCywFFyhw5YpAHmKc2CQIHw+SsVngcPrmVAAtvCZQRNV5zR61ICipckNEXnya8/JGa34ntyELxWDradY74726OlJSgszpHbAOMK02C4yx7OU32GWlPlsZBUGAqS5Tu4MSjlD1f/eQBsFx/pn8deP4yuR1294DTP7dsZ9ml64ZlcIlg=='

换行;

signed_request = base64.b64encode(rsa.sign(canonical_request.encode(), privkey, "SHA-1"))

signed_request = base64.b64encode(pure_sign(canonical_request.encode(), privkey))

我最近为 python 2.7 和 3.x 编写了一个 chef 客户端库,构建于 pyca/cryptography and requests. It includes built-in support for Chef's authentication protocol:

之上

https://github.com/samstav/okchef

>>> import chef
>>>
>>> client = chef.ChefClient('https://api.opscode.com')
>>> client.authenticate('chef-user', '~/chef-user.pem')
>>> response = client.get('/users/chef-user')
>>> print(response.json())
{'display_name': 'chef-user',
 'email': 'chef-user@example.com',
 'first_name': 'Chef',
 'last_name': 'User',
 'middle_name': '',
 'public_key': '-----BEGIN PUBLIC KEY-----\nMIIBIj...IDAQAB\n-----END PUBLIC KEY-----\n',
 'username': 'chef-user'}

我为处理 rsa/authentication 位的代码创建了一个单独的存储库:

https://github.com/samstav/requests-chef

身份验证实现的具体细节在此文件中:

https://github.com/samstav/requests-chef/blob/master/requests_chef/mixlib_auth.py