Replicating a Chef Request with Python RSA

Goal: I need a Python 3 wrapper for the Chef REST API. Because its Python-3, PyChef is out of the question.

Problem: I am trying to replicate a Chef request using RSA Python. But I get an error message in the wrapper: Invalid signature for user or client 'XXX'.

I went to a wrapper trying to replicate the cURL script shown in Chef Authentication and Authorization with cURL using the RSA Python package: RSA Signing and Verification .

Here is my rewrite. It might be easier, but I started getting paranoid messages about line breaks and header order, so I added a few unnecessary things:

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/"

hashed_body = base64.b64encode(hashlib.sha1(body.encode()).digest()).decode("ASCII")
hashed_path = base64.b64encode(hashlib.sha1(path.encode()).digest()).decode("ASCII")
timestamp ="%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 =
    privkey = rsa.PrivateKey.load_pkcs1(keydata)

with open("pubkey.pem", 'rb') as pubfile:
    keydata =
    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"))


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)

# 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)


At each step I tried to check if the variables have the same results as their counterparts in the curl script.

The problem seems to be at the signing stage - there is a clear mismatch between the python output packages and my mac openssl tools. Due to this discrepancy, the Chief returns {"error":["Invalid signature for user or client 'anton'"]}

. Curl script with the same values ​​and keys works fine.

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

from Python matters



whereas the output echo -n "hello" | openssl rsautl -sign -inkey ~/.chef/anton.pem | openssl enc -base64




I could not find information on the default hashing algorithm in openssl for rsautl

, but I think it is SHA-1.

At this point I really don't know what way to look like, hope someone can help make it right.


source to share

3 answers

From Chef Authentication and Authorization with cURL ,

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


the time is in UTC, so in Python it should be

timestamp ="%Y-%m-%dT%H:%M:%SZ")  



Python equivalent,

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


there is

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


In Python code, you sign the message with the SHA-1 message of the message. This is called a separate signature.

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

but this sign signs the entire message without digesting.

Python module rsa

has no equivalent openssl rsautl -sign

. Therefore, I have defined a function to fill this 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



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







Change the line;

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



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




I recently wrote a chef client library for python 2.7 and 3.x built on top of pyca / cryptography and requests . It includes built-in support for the chef authentication protocol :

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


I created a separate repository for the code that handles the rsa / authentication bits:

The nuts and bolts for the auth implementation are in this file:



You can automate your interaction with the chef - using these tools:


As pointed out jww

, the OP mentioned that he doesn't want to use Selenium.
However, I wanted my answer to be complete (it can be used by others (including me) besides the OP), I included Selenium in the list.



All Articles