From a36ade1db581c58babe2bf3b41fd7fa099b7482b Mon Sep 17 00:00:00 2001 From: Hoylen Sue Date: Wed, 18 Jan 2017 18:26:06 +1000 Subject: [PATCH 1/5] Added support for RSA-SHA1 signature method. --- README.rst | 32 +++++++++++++++++++++---- httpie_oauth.py | 64 ++++++++++++++++++++++++++++++++++++++++++++----- setup.py | 2 +- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 58c52cd..3f2f066 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,7 @@ httpie-oauth -=========== +============ -OAuth plugin for `HTTPie `_. - -It currently provides support for OAuth 1.0a 2-legged. +OAuth 1.0a two-legged plugin for `HTTPie `_. Installation @@ -20,10 +18,35 @@ You should now see ``oauth1`` under ``--auth-type`` in ``$ http --help`` output. Usage ----- +HMAC-SHA1 +......... + +To use the HMAC-SHA1 signature method, in the ``--auth`` parameter +provide the client-key, a single colon and the client-secret. + .. code-block:: bash $ http --auth-type=oauth1 --auth='client-key:client-secret' example.org +It will interactively prompt for the client-secret, if there is no colon. + +RSA-SHA1 +........ + +To use the RSA-SHA1 signature method, in the ``--auth`` parameter +provide the client-key, two colons and the name of a file containing +the RSA private key. The file must contain a PEM formatted RSA private +key. + +.. code-block:: bash + + $ http --auth-type=oauth1 --auth='client-key::filename' example.org + +It will interactively prompt for the filename, if there is no value +after the two colons. + +HTTPie Sessions +............... You can also use `HTTPie sessions `_: @@ -34,4 +57,3 @@ You can also use `HTTPie sessions `_: # Re-use auth $ http --session=logged-in POST example.org hello=world - diff --git a/httpie_oauth.py b/httpie_oauth.py index 47cdc45..4809634 100644 --- a/httpie_oauth.py +++ b/httpie_oauth.py @@ -1,12 +1,25 @@ """ -OAuth plugin for HTTPie. +OAuth 1.0a 2-legged plugin for HTTPie. + +Supports HMAC-SHA1 and RSA-SHA1 signature methods. + +If the authentication parameter is "username:password" then HMAC-SHA1 is used. +If the password is omitted (i.e. --auth username is provided), the user is +prompted for the password. + +If the authentication parameter is "username::filename" (double colon between +the username and the filename) then RSA-SHA1 is used, and the PEM formatted +private key is read from that file. If the filename is omitted +(i.e. --auth username:: is provided), the user is prompted for +the filename. The username is used as the oauth_client_key OAuth parameter. """ +import sys from httpie.plugins import AuthPlugin from requests_oauthlib import OAuth1 +from oauthlib.oauth1 import SIGNATURE_RSA - -__version__ = '1.0.2' +__version__ = '2.0.0' __author__ = 'Jakub Roztocil' __licence__ = 'BSD' @@ -15,7 +28,46 @@ class OAuth1Plugin(AuthPlugin): name = 'OAuth 1.0a 2-legged' auth_type = 'oauth1' - description = '' + description =\ + '--auth user:HMAC-SHA1_secret or --auth user::RSA-SHA1_privateKeyFile' + + def get_auth(self, username=None, password=None): + if not password.startswith(':'): + # HMAC-SHA1 signature method (--auth username:password) + return OAuth1(client_key=username, client_secret=password) + + else: + # RSA-SHA1 signature method (--auth username::filename) + filename = password[1:] + if len(filename) == 0: + # Prompt for filename of RSA private key + try: + filename = raw_input('http: filename of RSA private key: ') + except EOFError: # if ^D entered + sys.exit(1) + + try: + key = open(filename).read() + + # Crude checks for correct file contents + + if key.find('-----BEGIN RSA PRIVATE KEY-----') == -1: + if key.find('-----BEGIN PUBLIC KEY-----') == -1: + err = 'does not contain a PEM formatted private key' + else: + err = 'wrong key, please provide the PRIVATE key' + elif key.find('-----END RSA PRIVATE KEY-----') == -1: + err = 'private key is incomplete' + else: + err = None + if err is not None: + sys.stderr.write("http: " + filename + ': ' + err) + sys.exit(1) + + return OAuth1(client_key=username, + signature_method=SIGNATURE_RSA, + rsa_key=key) - def get_auth(self, username, password): - return OAuth1(client_key=username, client_secret=password) + except IOError as e: + sys.stderr.write("http: " + str(e)) + sys.exit(1) diff --git a/setup.py b/setup.py index 33f58f8..2d779d2 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ name='httpie-oauth', description='OAuth plugin for HTTPie.', long_description=open('README.rst').read().strip(), - version='1.0.2', + version='2.0.0', author='Jakub Roztocil', author_email='jakub@roztocil.name', license='BSD', From 6c0d4a806b7f18c08bf9dab9096c1c622ad109d7 Mon Sep 17 00:00:00 2001 From: Hoylen Sue Date: Thu, 19 Jan 2017 13:28:14 +1000 Subject: [PATCH 2/5] Improved errors and documented passphrase protected private keys are not supported. --- README.rst | 6 ++++ httpie_oauth.py | 87 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index 3f2f066..c570860 100644 --- a/README.rst +++ b/README.rst @@ -29,6 +29,8 @@ provide the client-key, a single colon and the client-secret. $ http --auth-type=oauth1 --auth='client-key:client-secret' example.org It will interactively prompt for the client-secret, if there is no colon. +If the password starts with a colon, use this interactive method to enter it +(otherwise the extra colon will cause it to use RSA-SHA1 instead of HMAC-SHA1). RSA-SHA1 ........ @@ -45,6 +47,10 @@ key. It will interactively prompt for the filename, if there is no value after the two colons. +The filename can also be a relative or absolute path to the file. + +Passphrase protected private keys are not supported. + HTTPie Sessions ............... diff --git a/httpie_oauth.py b/httpie_oauth.py index 4809634..27d1cda 100644 --- a/httpie_oauth.py +++ b/httpie_oauth.py @@ -32,6 +32,24 @@ class OAuth1Plugin(AuthPlugin): '--auth user:HMAC-SHA1_secret or --auth user::RSA-SHA1_privateKeyFile' def get_auth(self, username=None, password=None): + """ + Generate OAuth 1.0a 2-legged authentication for HTTPie. + + Note: Passpharse protected private keys are not yet supported. + Before support can be implemented, the PyJWT, oauthlib and + requests_oauthlib modules need to be updated. + The passphrase needs to be obtained here and passed + through to PyJWT's jwt/algorithms.py, line 168, where currently it + passes into load_pem_private_key a hardcoded value of None for the + password. + To get it to there, many places in oauthlib's oauth1/rfc5849/__init__.py + and oauth1/rfc5849/signature.py, as well as in requests_oauthlib's + oauth1_auth.py, need to be updated to pass it through. + + :param username: username + :param password: password, or colon followed by a filename + :return: requests_oauthlib.oauth1_auth.OAuth1 object + """ if not password.startswith(':'): # HMAC-SHA1 signature method (--auth username:password) return OAuth1(client_key=username, client_secret=password) @@ -46,28 +64,57 @@ def get_auth(self, username=None, password=None): except EOFError: # if ^D entered sys.exit(1) - try: - key = open(filename).read() + key = OAuth1Plugin.read_private_key(filename) - # Crude checks for correct file contents + return OAuth1(client_key=username, + signature_method=SIGNATURE_RSA, + rsa_key=key) - if key.find('-----BEGIN RSA PRIVATE KEY-----') == -1: - if key.find('-----BEGIN PUBLIC KEY-----') == -1: - err = 'does not contain a PEM formatted private key' - else: - err = 'wrong key, please provide the PRIVATE key' - elif key.find('-----END RSA PRIVATE KEY-----') == -1: - err = 'private key is incomplete' - else: - err = None - if err is not None: - sys.stderr.write("http: " + filename + ': ' + err) - sys.exit(1) + @staticmethod + def read_private_key(filename): + """ + Check if the key is a recognised private key format. + + Prints an error message to stderr and exits if it is not. Uses + crude checks to try and generate more useful error messages. - return OAuth1(client_key=username, - signature_method=SIGNATURE_RSA, - rsa_key=key) + :param filename: file to read private key from + :return: PEM formatted private key + """ + try: + key = open(filename).read() - except IOError as e: - sys.stderr.write("http: " + str(e)) + if key.find('-----BEGIN RSA PRIVATE KEY-----') == -1: + # Did not find the start of private key. + + if key.find('-----BEGIN PUBLIC KEY-----') != -1 or \ + key.find('-----BEGIN RSA PUBLIC KEY-----') != -1 or \ + key.find('ssh-rsa ') != -1 or \ + key.find('---- BEGIN SSH2 PUBLIC KEY ----') != -1: + # Appears to contain a PKCS8, PEM, OpenSSH old or + # OpenSSH new format public key. + # The newer OpenSSH format does not follow RFC7468 + # and only has 4 hyphens and spaces around the text! + err = 'wrong key, please provide the PRIVATE key' + elif key.find('-----BEGIN OPENSSH PRIVATE KEY-----') != -1: + # Appears to contain newer OpenSSH private key format + err = 'private key format not supported' + \ + ', PEM format required' + else: + # Generic error message + err = 'does not contain a PEM formatted private key' + + elif key.find('-----END RSA PRIVATE KEY-----') == -1: + # Found start of private key, but not its end + err = 'private key is incomplete' + else: + err = None + if err is not None: + sys.stderr.write("http: " + filename + ': ' + err) sys.exit(1) + + return key + + except IOError as e: + sys.stderr.write("http: " + str(e)) + sys.exit(1) From d64adfb959bb3b59d24f8a985e3e486035a1c80d Mon Sep 17 00:00:00 2001 From: Hoylen Sue Date: Fri, 20 Jan 2017 15:06:20 +1000 Subject: [PATCH 3/5] Client key can be read in from the private key file. --- README.rst | 26 ++++++++++++++- httpie_oauth.py | 87 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index c570860..ce59220 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ RSA-SHA1 ........ To use the RSA-SHA1 signature method, in the ``--auth`` parameter -provide the client-key, two colons and the name of a file containing +provide the client key, two colons and the name of a file containing the RSA private key. The file must contain a PEM formatted RSA private key. @@ -51,6 +51,30 @@ The filename can also be a relative or absolute path to the file. Passphrase protected private keys are not supported. +Providing the client key in the private key file +++++++++++++++++++++++++++++++++++++++++++++++++ + +If the client key in the `--auth`` parameter is empty (i.e. the option +argument is just two colons and the filename), the +`oauth_consumer_key` parameter from the file is used. It must appear +in the file before the private key. + +For example, if the private key file contains something like this: + +.. code-block + + oauth_consumer_key: myconsumerkey + -----BEGIN RSA PRIVATE KEY----- + ... + -----END RSA PRIVATE KEY----- + +It can be used with this command: + +.. code-block:: bash + + $ http --auth-type=oauth1 --auth=::filename example.org + + HTTPie Sessions ............... diff --git a/httpie_oauth.py b/httpie_oauth.py index 27d1cda..acec84d 100644 --- a/httpie_oauth.py +++ b/httpie_oauth.py @@ -14,6 +14,7 @@ the filename. The username is used as the oauth_client_key OAuth parameter. """ +import string import sys from httpie.plugins import AuthPlugin from requests_oauthlib import OAuth1 @@ -51,11 +52,11 @@ def get_auth(self, username=None, password=None): :return: requests_oauthlib.oauth1_auth.OAuth1 object """ if not password.startswith(':'): - # HMAC-SHA1 signature method (--auth username:password) + # HMAC-SHA1 signature method (--auth client-key:client-secret) return OAuth1(client_key=username, client_secret=password) else: - # RSA-SHA1 signature method (--auth username::filename) + # RSA-SHA1 signature method (--auth oauth_consumer_key::filename) filename = password[1:] if len(filename) == 0: # Prompt for filename of RSA private key @@ -64,39 +65,47 @@ def get_auth(self, username=None, password=None): except EOFError: # if ^D entered sys.exit(1) - key = OAuth1Plugin.read_private_key(filename) + username, key = OAuth1Plugin.read_private_key(username, filename) return OAuth1(client_key=username, signature_method=SIGNATURE_RSA, rsa_key=key) @staticmethod - def read_private_key(filename): + def read_private_key(username, filename): """ Check if the key is a recognised private key format. Prints an error message to stderr and exits if it is not. Uses crude checks to try and generate more useful error messages. + :param username: username to use :param filename: file to read private key from :return: PEM formatted private key """ + PEM_PRIVATE_KEY_BEGINNING = '-----BEGIN RSA PRIVATE KEY-----' + PEM_PRIVATE_KEY_ENDING = '-----END RSA PRIVATE KEY-----' + ATTR_NAME = 'oauth_consumer_key' + try: - key = open(filename).read() + data = open(filename).read() + + key_start = data.find(PEM_PRIVATE_KEY_BEGINNING) + key_end = data.find(PEM_PRIVATE_KEY_ENDING) - if key.find('-----BEGIN RSA PRIVATE KEY-----') == -1: + if key_start == -1: # Did not find the start of private key. - if key.find('-----BEGIN PUBLIC KEY-----') != -1 or \ - key.find('-----BEGIN RSA PUBLIC KEY-----') != -1 or \ - key.find('ssh-rsa ') != -1 or \ - key.find('---- BEGIN SSH2 PUBLIC KEY ----') != -1: + if data.find('-----BEGIN PUBLIC KEY-----') != -1 or \ + data.find('-----BEGIN RSA PUBLIC KEY-----') != -1 or \ + data.find('ssh-rsa ') != -1 or \ + data.find('---- BEGIN SSH2 PUBLIC KEY ----') != -1: # Appears to contain a PKCS8, PEM, OpenSSH old or # OpenSSH new format public key. # The newer OpenSSH format does not follow RFC7468 # and only has 4 hyphens and spaces around the text! err = 'wrong key, please provide the PRIVATE key' - elif key.find('-----BEGIN OPENSSH PRIVATE KEY-----') != -1: + elif data.find('-----BEGIN OPENSSH PRIVATE KEY-----') != -1: # Appears to contain newer OpenSSH private key format err = 'private key format not supported' + \ ', PEM format required' @@ -104,17 +113,63 @@ def read_private_key(filename): # Generic error message err = 'does not contain a PEM formatted private key' - elif key.find('-----END RSA PRIVATE KEY-----') == -1: - # Found start of private key, but not its end - err = 'private key is incomplete' else: - err = None + if key_end == -1: + err = 'private key is incomplete' + else: + key_end += len(PEM_PRIVATE_KEY_ENDING) + err = None + + # If the username is blank, try to extract a username from the file + + if len(username) == 0 and err is None: + username, err = OAuth1Plugin.extract_username(data, ATTR_NAME, + 0, key_start) + if err is not None: sys.stderr.write("http: " + filename + ': ' + err) sys.exit(1) - return key + pem_key = data[key_start:key_end] + + return username, pem_key except IOError as e: sys.stderr.write("http: " + str(e)) sys.exit(1) + + @staticmethod + def extract_username(data, attr_name, start, end, limit=8096): + """ + Extract a named parameter from the contents. + + :param data: text to search + :param attr_name: name of attribute to look for + :param start: index into contents of where to start looking + :param end: index into contents of where to stop looking + :param limit: upper limit for end position + :return: client-key value or None if not found + """ + stop_pos = end + if limit < end: + stop_pos = limit + i = start + + while i < stop_pos: + eol_pos = data.find('\n', i, stop_pos) + if eol_pos == -1: + break # no more complete lines found + + colon_pos = data.find(':', i, eol_pos) + if colon_pos != -1: + name = data[i:colon_pos].strip() + value = data[colon_pos + 1: eol_pos].strip() + if name == attr_name: + return value, None # successfully found + i = eol_pos + 1 + + if limit < end: + return None, '"{}" not found in first {} characters'.format( + attr_name, limit) + else: + return None, '"{}" not found before private key'.format(attr_name) From 156fa5bc0c3e78ee809fc27376a2e9012624c4da Mon Sep 17 00:00:00 2001 From: Hoylen Sue Date: Fri, 20 Jan 2017 15:09:21 +1000 Subject: [PATCH 4/5] Fixed formatting. --- README.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index ce59220..2c3d481 100644 --- a/README.rst +++ b/README.rst @@ -51,17 +51,17 @@ The filename can also be a relative or absolute path to the file. Passphrase protected private keys are not supported. -Providing the client key in the private key file +Including the client key in the private key file ++++++++++++++++++++++++++++++++++++++++++++++++ -If the client key in the `--auth`` parameter is empty (i.e. the option -argument is just two colons and the filename), the -`oauth_consumer_key` parameter from the file is used. It must appear -in the file before the private key. +If the client key in the ``--auth`` parameter is empty (i.e. the +option argument is just two colons and the filename), the +``oauth_consumer_key`` parameter from the file is used. It must +appear in the file before the private key. For example, if the private key file contains something like this: -.. code-block +:: oauth_consumer_key: myconsumerkey -----BEGIN RSA PRIVATE KEY----- From 58622c768ef5412eff3248ccbef51bc25ebb9eff Mon Sep 17 00:00:00 2001 From: Hoylen Sue Date: Mon, 23 Jan 2017 21:25:43 +1000 Subject: [PATCH 5/5] Documented dependencies required to support RSA-SHA1. --- README.rst | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2c3d481..59c6641 100644 --- a/README.rst +++ b/README.rst @@ -12,8 +12,20 @@ Installation $ pip install httpie-oauth -You should now see ``oauth1`` under ``--auth-type`` in ``$ http --help`` output. +You should now see ``oauth1`` under ``--auth-type`` in the +``$ http --help`` output. +To be able to use the RSA-SHA1 signature type, also install **PyJWT** +and PyCA's **cryptography** package. + +.. code-block:: bash + + $ pip install pyjwt + $ pip install cryptography + +On CentOS 7, it might be easier to use *yum* to install "epel-release" +and then the "python2-cryptography" packages, since to *pip install* it +requires C code to be compiled. Usage ----- @@ -87,3 +99,17 @@ You can also use `HTTPie sessions `_: # Re-use auth $ http --session=logged-in POST example.org hello=world + + +Troubleshooting +............... + +ImportError: No module named jwt.algorithms ++++++++++++++++++++++++++++++++++++++++++++ + +The *PyJWT* module is not available. Please install it. + +AttributeError: 'module' object has no attribute 'RSAAlgorithm' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +The PyCA's *cryptography* module is not available. Please install it.