summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAsif Saif Uddin <auvipy@gmail.com>2020-06-06 08:07:20 +0600
committerGitHub <noreply@github.com>2020-06-06 08:07:20 +0600
commit72269341361ef50b1bcd920740bdb1983f6a7337 (patch)
tree9362bb4efa6f849956ddf44b5a701c0fc8f8c7b2
parentdc4d464bc83588d345e021398618fc1da2705fe1 (diff)
parentbda81b3cb6306dec19a6e60113e21b2933d0950c (diff)
downloadoauthlib-doc-dynreg.tar.gz
Merge branch 'master' into doc-dynregdoc-dynreg
-rw-r--r--CHANGELOG.rst4
-rw-r--r--docs/faq.rst2
-rw-r--r--docs/feature_matrix.rst101
-rw-r--r--docs/installation.rst120
-rw-r--r--docs/oauth2/endpoints/metadata.rst2
-rw-r--r--oauthlib/oauth1/__init__.py33
-rw-r--r--oauthlib/oauth1/rfc5849/__init__.py50
-rw-r--r--oauthlib/oauth1/rfc5849/endpoints/base.py61
-rw-r--r--oauthlib/oauth1/rfc5849/signature.py874
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/metadata.py8
-rwxr-xr-xsetup.py6
-rw-r--r--tests/oauth1/rfc5849/test_signatures.py1145
12 files changed, 1604 insertions, 802 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c42df83..478c2ee 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -47,6 +47,10 @@ OAuth1.0 Client
* #669: Add case-insensitive headers to oauth1 `BaseEndpoint`
+OAuth1.0
+
+ * #722: Added support for HMAC-SHA512, RSA-SHA256 and RSA-SHA512 signature methods.
+
3.0.2 (2019-07-04)
------------------
* #650: Fixed space encoding in base string URI used in the signature base string.
diff --git a/docs/faq.rst b/docs/faq.rst
index d9cd5c6..4814dcd 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -12,7 +12,7 @@ What parts of OAuth 1 & 2 are supported?
See :doc:`feature_matrix`.
OAuth 1 with RSA-SHA1 signatures says "could not import cryptography". What should I do?
-----------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------
Install oauthlib with rsa flag or install cryptography manually via pip.
diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst
index df8cb0e..56d0cf3 100644
--- a/docs/feature_matrix.rst
+++ b/docs/feature_matrix.rst
@@ -1,33 +1,56 @@
Supported features and platforms
================================
-OAuth 1 is fully supported per the RFC for both clients and providers.
-Extensions and variations that are outside the spec are not supported.
+Features
+--------
-- HMAC-SHA1, RSA-SHA1 and plaintext signatures.
-- Signature placement in header, url or body.
+OAuth 1.0a
+..........
+
+OAuth 1.0a is fully supported for both clients and providers.
+
+All standard *signature methods* defined in `RFC 5849`_ *The OAuth 1.0
+Protocol* are supported:
+
+- HMAC-SHA1
+- RSA-SHA1
+- PLAINTEXT
+
+Non-standard *signature methods* that replaces SHA-1 with stronger
+digest algorithms are also supported:
+
+- HMAC-SHA256
+- HMAC-SHA512
+- RSA-SHA256
+- RSA-SHA512
+
+The OAuth 1.0a signature can be placed in the header, URL or body of
+the request.
+
+OAuth 2.0
+.........
OAuth 2.0 client and provider support for:
-- `RFC6749#section-4.1`_: Authorization Code Grant
-- `RFC6749#section-4.2`_: Implicit Grant
-- `RFC6749#section-4.3`_: Resource Owner Password Credentials Grant
-- `RFC6749#section-4.4`_: Client Credentials Grant
-- `RFC6749#section-6`_: Refresh Tokens
-- `RFC6750`_: Bearer Tokens
-- `RFC7009`_: Token Revocation
-- `RFC Draft MAC tokens`_
+- `RFC 6749 section-4.1`_: Authorization Code Grant
+- `RFC 6749 section-4.2`_: Implicit Grant
+- `RFC 6749 section-4.3`_: Resource Owner Password Credentials Grant
+- `RFC 6749 section-4.4`_: Client Credentials Grant
+- `RFC 6749 section-6`_: Refresh Tokens
+- `RFC 6750`_: Bearer Tokens
+- `RFC 7009`_: Token Revocation
+- `RFC Draft`_ Message Authentication Code (MAC) Tokens
- OAuth2.0 Provider: `OpenID Connect Core`_
-- OAuth2.0 Provider: `RFC7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
-- OAuth2.0 Provider: `RFC7662`_: Token Introspection
-- OAuth2.0 Provider: `RFC8414`_: Authorization Server Metadata
+- OAuth2.0 Provider: `RFC 7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
+- OAuth2.0 Provider: `RFC 7662`_: Token Introspection
+- OAuth2.0 Provider: `RFC 8414`_: Authorization Server Metadata
Features to be implemented (any help/PR are welcomed):
- OAuth2.0 **Client**: `OpenID Connect Core`_
-- OAuth2.0 **Client**: `RFC7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
-- OAuth2.0 **Client**: `RFC7662`_: Token Introspection
-- OAuth2.0 **Client**: `RFC8414`_: Authorization Server Metadata
+- OAuth2.0 **Client**: `RFC 7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
+- OAuth2.0 **Client**: `RFC 7662`_: Token Introspection
+- OAuth2.0 **Client**: `RFC 8414`_: Authorization Server Metadata
- SAML2
- Bearer JWT as Client Authentication
- Dynamic client registration
@@ -35,24 +58,32 @@ Features to be implemented (any help/PR are welcomed):
- OpenID Session Management
- ...and more
-Supported platforms
--------------------
+Platforms
+---------
+
+OAuthLib is mainly developed and tested on 64-bit Linux. It works on
+Unix and Unix-like operating systems (including macOS), as well as
+Microsoft Windows.
+
+It should work on any platform that supports Python, if features
+requiring RSA public-key cryptography is not used.
-OAuthLib is mainly developed/tested on 64 bit Linux but works on Unix (incl. OS
-X) and Windows as well. Unless you are using the RSA features of OAuth 1 you
-should be able to use OAuthLib on any platform that supports Python. If you use
-RSA you are limited to the platforms supported by `cryptography`_.
+If features requiring RSA public-key cryptography is used (e.g
+RSA-SHA1 and RS256), it should work on any platform supported by
+PyCA's `cryptography`_ package. RSA features require installing
+additional packages: see the installation instructions for details.
.. _`cryptography`: https://cryptography.io/en/latest/installation/
-.. _`RFC6749#section-4.1`: https://tools.ietf.org/html/rfc6749#section-4.1
-.. _`RFC6749#section-4.2`: https://tools.ietf.org/html/rfc6749#section-4.2
-.. _`RFC6749#section-4.3`: https://tools.ietf.org/html/rfc6749#section-4.3
-.. _`RFC6749#section-4.4`: https://tools.ietf.org/html/rfc6749#section-4.4
-.. _`RFC6749#section-6`: https://tools.ietf.org/html/rfc6749#section-6
-.. _`RFC6750`: https://tools.ietf.org/html/rfc6750
-.. _`RFC Draft MAC tokens`: https://tools.ietf.org/id/draft-ietf-oauth-v2-http-mac-02.html
-.. _`RFC7009`: https://tools.ietf.org/html/rfc7009
-.. _`RFC7662`: https://tools.ietf.org/html/rfc7662
-.. _`RFC7636`: https://tools.ietf.org/html/rfc7636
+.. _`RFC 5849`: https://tools.ietf.org/html/rfc5849
+.. _`RFC 6749 section-4.1`: https://tools.ietf.org/html/rfc6749#section-4.1
+.. _`RFC 6749 section-4.2`: https://tools.ietf.org/html/rfc6749#section-4.2
+.. _`RFC 6749 section-4.3`: https://tools.ietf.org/html/rfc6749#section-4.3
+.. _`RFC 6749 section-4.4`: https://tools.ietf.org/html/rfc6749#section-4.4
+.. _`RFC 6749 section-6`: https://tools.ietf.org/html/rfc6749#section-6
+.. _`RFC 6750`: https://tools.ietf.org/html/rfc6750
+.. _`RFC Draft`: https://tools.ietf.org/id/draft-ietf-oauth-v2-http-mac-02.html
+.. _`RFC 7009`: https://tools.ietf.org/html/rfc7009
+.. _`RFC 7662`: https://tools.ietf.org/html/rfc7662
+.. _`RFC 7636`: https://tools.ietf.org/html/rfc7636
.. _`OpenID Connect Core`: https://openid.net/specs/openid-connect-core-1_0.html
-.. _`RFC8414`: https://tools.ietf.org/html/rfc8414
+.. _`RFC 8414`: https://tools.ietf.org/html/rfc8414
diff --git a/docs/installation.rst b/docs/installation.rst
index 72d7b08..0e00e39 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -1,71 +1,147 @@
Installing OAuthLib
===================
-The recommended way to install OAuthLib is from PyPI but if you are running
-into a bug or want to try out recently implemented features you will want to
-try installing directly from the GitHub master branch.
-For various reasons you may wish to install using your OS packaging system and
-install instructions for a few are shown below. Please send a PR to add a
-missing one.
+Install from PyPI
+-----------------
+
+The recommended way to install OAuthLib is from PyPI using the *pip*
+program. Either just the *standard install* by itself or *with extras
+for RSA*.
-Latest release on PyPI
-----------------------
+Standard install
+^^^^^^^^^^^^^^^^
+A standard installation contains the core features of OAuthLib. It can
+be installed by running:
.. code-block:: bash
pip install oauthlib
-Bleeding edge from GitHub master
---------------------------------
+To reduce its requirements, the Python packages needed for RSA
+public-key cryptography are not included in the standard installation.
+
+
+With extras for RSA
+^^^^^^^^^^^^^^^^^^^
+
+To support features that use RSA public-key cryptography, PyCA's
+`cryptography`_ package and the `PyJWT`_ package must also be
+installed. This can be done by installing the core features of
+OAuthLib along with the "signedtoken" extras.
.. code-block:: bash
- pip install -e git+https://github.com/oauthlib/oauthlib.git#egg=oauthlib
+ pip install 'oauthlib[signedtoken]'
+
+Note: the quotes may be required, since shells can interpret the
+square brackets as special characters.
+
+Alternatively, those two Python packages can be installed manually by
+running ``pip install cryptography`` and ``pip install pyjwt``, either
+before or after installing the standard installation of OAuthLib.
+PyJWT depends on cryptography, so just installing *pyjwt* should
+automatically also install *cryptography*. But *cryptography* has
+dependencies that can cause its installation to fail, so it can be
+better to get it installed before installing PyJWT.
+
+Install from operating system distribution
+------------------------------------------
+
+Alternatively, install it from the operating system distribution's
+packaging system, if OAuthLib is available as a distribution package.
+Install instructions for some distributions are shown below.
+
+The distribution packages usually only contain the standard install of
+OAuthLib. To enable support for RSA, the *cryptography* and *pyjwt*
+Python packages also need to be installed: either from the
+distribution packages (if available) or from PyPI.
Debian and derivatives like Ubuntu, Mint, etc.
----------------------------------------------
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
- apt-get install python-oauthlib
apt-get install python3-oauthlib
-Redhat and Fedora
------------------
+The Python2 package is called "python-oauthlib".
+
+RHEL, CentOS and Fedora
+^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
- yum install python-oauthlib
yum install python3-oauthlib
+The Python2 package is called "python2-oauthlib", and is available on
+some distributions (e.g.Fedora 31 and CentOS 7) but not available on
+others (e.g. CentOS 8).
+
+For CentOS, the Python3 package is only available on CentOS 8 and
+higher.
+
openSUSE
---------
+^^^^^^^^
.. code-block:: bash
- zypper in python-oauthlib
zypper in python3-oauthlib
+The Python2 package is called "python-oauthlib".
+
Gentoo
-------
+^^^^^^
.. code-block:: bash
emerge oauthlib
Arch
-----
+^^^^
.. code-block:: bash
pacman -S python-oauthlib
- pacman -S python2-oauthlib
+
+The Python2 package is called "python2-oauthlib".
FreeBSD
--------
+^^^^^^^
.. code-block:: bash
pkg_add -r security/py-oauthlib/
+
+
+Install from GitHub
+-------------------
+
+Alternatively, install it directly from the source repository on
+GitHub. This is the "bleading edge" version, but it may be useful for
+accessing bug fixes and/or new features that have not been released.
+
+Standard install
+^^^^^^^^^^^^^^^^
+
+The standard installation contains the core features of OAuthLib.
+
+.. code-block:: bash
+
+ pip install -e git+https://github.com/oauthlib/oauthlib.git#egg=oauthlib
+
+With extras for RSA
+^^^^^^^^^^^^^^^^^^^
+
+To support features that use RSA public-key cryptography, install the
+core features of OAuthLib along with the "signedtoken" extras.
+
+.. code-block:: bash
+
+ pip install -e 'git+https://github.com/oauthlib/oauthlib.git#egg=oauthlib[signedtoken]'
+
+Note: the quotes may be required, since shells can interpret the
+square brackets as special characters.
+
+.. _`cryptography`: https://cryptography.io/
+.. _`PyJWT`: https://pyjwt.readthedocs.io/
diff --git a/docs/oauth2/endpoints/metadata.rst b/docs/oauth2/endpoints/metadata.rst
index d44e8b7..a879765 100644
--- a/docs/oauth2/endpoints/metadata.rst
+++ b/docs/oauth2/endpoints/metadata.rst
@@ -2,7 +2,7 @@
Metadata endpoint
===================
-OAuth2.0 Authorization Server Metadata (`RFC8414`_) endpoint provide the metadata of your authorization server. Since the metadata results can be a combination of OAuthlib's Endpoint (see :doc:`preconfigured_servers`), the MetadataEndpoint's class takes a list of Endpoints in parameter, and aggregate the metadata in the response.
+OAuth2.0 Authorization Server Metadata (`RFC8414`_) endpoint provide the metadata of your authorization server. Since the metadata results can be a combination of OAuthlib's Endpoint (see :doc:`/oauth2/preconfigured_servers`), the MetadataEndpoint's class takes a list of Endpoints in parameter, and aggregate the metadata in the response.
See below an example of usage with `bottle-oauthlib`_ when using a `LegacyApplicationServer` (password grant) endpoint:
diff --git a/oauthlib/oauth1/__init__.py b/oauthlib/oauth1/__init__.py
index 224fecf..07ef422 100644
--- a/oauthlib/oauth1/__init__.py
+++ b/oauthlib/oauth1/__init__.py
@@ -5,17 +5,24 @@ oauthlib.oauth1
This module is a wrapper for the most recent implementation of OAuth 1.0 Client
and Server classes.
"""
-from .rfc5849 import (
- SIGNATURE_HMAC, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256,
- SIGNATURE_PLAINTEXT, SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER,
- SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, Client,
-)
-from .rfc5849.endpoints import (
- AccessTokenEndpoint, AuthorizationEndpoint, RequestTokenEndpoint,
- ResourceEndpoint, SignatureOnlyEndpoint, WebApplicationServer,
-)
-from .rfc5849.errors import (
- InsecureTransportError, InvalidClientError, InvalidRequestError,
- InvalidSignatureMethodError, OAuth1Error,
-)
+from .rfc5849 import Client
+from .rfc5849 import (SIGNATURE_HMAC,
+ SIGNATURE_HMAC_SHA1,
+ SIGNATURE_HMAC_SHA256,
+ SIGNATURE_HMAC_SHA512,
+ SIGNATURE_RSA,
+ SIGNATURE_RSA_SHA1,
+ SIGNATURE_RSA_SHA256,
+ SIGNATURE_RSA_SHA512,
+ SIGNATURE_PLAINTEXT)
+from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY
+from .rfc5849 import SIGNATURE_TYPE_BODY
from .rfc5849.request_validator import RequestValidator
+from .rfc5849.endpoints import RequestTokenEndpoint, AuthorizationEndpoint
+from .rfc5849.endpoints import AccessTokenEndpoint, ResourceEndpoint
+from .rfc5849.endpoints import SignatureOnlyEndpoint, WebApplicationServer
+from .rfc5849.errors import (InsecureTransportError,
+ InvalidClientError,
+ InvalidRequestError,
+ InvalidSignatureMethodError,
+ OAuth1Error)
diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py
index f7cd3f3..c559251 100644
--- a/oauthlib/oauth1/rfc5849/__init__.py
+++ b/oauthlib/oauth1/rfc5849/__init__.py
@@ -4,6 +4,19 @@ oauthlib.oauth1.rfc5849
This module is an implementation of various logic needed
for signing and checking OAuth 1.0 RFC 5849 requests.
+
+It supports all three standard signature methods defined in RFC 5849:
+
+- HMAC-SHA1
+- RSA-SHA1
+- PLAINTEXT
+
+It also supports signature methods that are not defined in RFC 5849. These are
+based on the standard ones but replace SHA-1 with the more secure SHA-256:
+
+- HMAC-SHA256
+- RSA-SHA256
+
"""
import base64
import hashlib
@@ -18,14 +31,38 @@ from . import parameters, signature
log = logging.getLogger(__name__)
-
+# Available signature methods
+#
+# Note: SIGNATURE_HMAC and SIGNATURE_RSA are kept for backward compatibility
+# with previous versions of this library, when it the only HMAC-based and
+# RSA-based signature methods were HMAC-SHA1 and RSA-SHA1. But now that it
+# supports other hashing algorithms besides SHA1, explicitly identifying which
+# hashing algorithm is being used is recommended.
+#
+# Note: if additional values are defined here, don't forget to update the
+# imports in "../__init__.py" so they are available outside this module.
SIGNATURE_HMAC_SHA1 = "HMAC-SHA1"
SIGNATURE_HMAC_SHA256 = "HMAC-SHA256"
-SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1
-SIGNATURE_RSA = "RSA-SHA1"
+SIGNATURE_HMAC_SHA512 = "HMAC-SHA512"
+SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1 # deprecated variable for HMAC-SHA1
+
+SIGNATURE_RSA_SHA1 = "RSA-SHA1"
+SIGNATURE_RSA_SHA256 = "RSA-SHA256"
+SIGNATURE_RSA_SHA512 = "RSA-SHA512"
+SIGNATURE_RSA = SIGNATURE_RSA_SHA1 # deprecated variable for RSA-SHA1
+
SIGNATURE_PLAINTEXT = "PLAINTEXT"
-SIGNATURE_METHODS = (SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
+
+SIGNATURE_METHODS = (
+ SIGNATURE_HMAC_SHA1,
+ SIGNATURE_HMAC_SHA256,
+ SIGNATURE_HMAC_SHA512,
+ SIGNATURE_RSA_SHA1,
+ SIGNATURE_RSA_SHA256,
+ SIGNATURE_RSA_SHA512,
+ SIGNATURE_PLAINTEXT
+)
SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
SIGNATURE_TYPE_QUERY = 'QUERY'
@@ -40,7 +77,10 @@ class Client:
SIGNATURE_METHODS = {
SIGNATURE_HMAC_SHA1: signature.sign_hmac_sha1_with_client,
SIGNATURE_HMAC_SHA256: signature.sign_hmac_sha256_with_client,
- SIGNATURE_RSA: signature.sign_rsa_sha1_with_client,
+ SIGNATURE_HMAC_SHA512: signature.sign_hmac_sha512_with_client,
+ SIGNATURE_RSA_SHA1: signature.sign_rsa_sha1_with_client,
+ SIGNATURE_RSA_SHA256: signature.sign_rsa_sha256_with_client,
+ SIGNATURE_RSA_SHA512: signature.sign_rsa_sha512_with_client,
SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client
}
diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py
index 8103606..3a8c267 100644
--- a/oauthlib/oauth1/rfc5849/endpoints/base.py
+++ b/oauthlib/oauth1/rfc5849/endpoints/base.py
@@ -11,10 +11,12 @@ import time
from oauthlib.common import CaseInsensitiveDict, Request, generate_token
from .. import (
- CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256,
- SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY,
- SIGNATURE_TYPE_QUERY, errors, signature, utils,
-)
+ CONTENT_TYPE_FORM_URLENCODED,
+ SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_HMAC_SHA512,
+ SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512,
+ SIGNATURE_PLAINTEXT,
+ SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY,
+ SIGNATURE_TYPE_QUERY, errors, signature, utils)
class BaseEndpoint:
@@ -179,38 +181,65 @@ class BaseEndpoint:
def _check_signature(self, request, is_token_request=False):
# ---- RSA Signature verification ----
- if request.signature_method == SIGNATURE_RSA:
+ if request.signature_method == SIGNATURE_RSA_SHA1 or \
+ request.signature_method == SIGNATURE_RSA_SHA256 or \
+ request.signature_method == SIGNATURE_RSA_SHA512:
+ # RSA-based signature method
+
# The server verifies the signature per `[RFC3447] section 8.2.2`_
# .. _`[RFC3447] section 8.2.2`: https://tools.ietf.org/html/rfc3447#section-8.2.1
+
rsa_key = self.request_validator.get_rsa_key(
request.client_key, request)
- valid_signature = signature.verify_rsa_sha1(request, rsa_key)
+
+ if request.signature_method == SIGNATURE_RSA_SHA1:
+ valid_signature = signature.verify_rsa_sha1(request, rsa_key)
+ elif request.signature_method == SIGNATURE_RSA_SHA256:
+ valid_signature = signature.verify_rsa_sha256(request, rsa_key)
+ elif request.signature_method == SIGNATURE_RSA_SHA512:
+ valid_signature = signature.verify_rsa_sha512(request, rsa_key)
+ else:
+ valid_signature = False
# ---- HMAC or Plaintext Signature verification ----
else:
+ # Non-RSA based signature method
+
# Servers receiving an authenticated request MUST validate it by:
# Recalculating the request signature independently as described in
# `Section 3.4`_ and comparing it to the value received from the
# client via the "oauth_signature" parameter.
# .. _`Section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
+
client_secret = self.request_validator.get_client_secret(
request.client_key, request)
+
resource_owner_secret = None
if request.resource_owner_key:
if is_token_request:
- resource_owner_secret = self.request_validator.get_request_token_secret(
- request.client_key, request.resource_owner_key, request)
+ resource_owner_secret = \
+ self.request_validator.get_request_token_secret(
+ request.client_key, request.resource_owner_key,
+ request)
else:
- resource_owner_secret = self.request_validator.get_access_token_secret(
- request.client_key, request.resource_owner_key, request)
+ resource_owner_secret = \
+ self.request_validator.get_access_token_secret(
+ request.client_key, request.resource_owner_key,
+ request)
if request.signature_method == SIGNATURE_HMAC_SHA1:
- valid_signature = signature.verify_hmac_sha1(request,
- client_secret, resource_owner_secret)
+ valid_signature = signature.verify_hmac_sha1(
+ request, client_secret, resource_owner_secret)
elif request.signature_method == SIGNATURE_HMAC_SHA256:
- valid_signature = signature.verify_hmac_sha256(request,
- client_secret, resource_owner_secret)
+ valid_signature = signature.verify_hmac_sha256(
+ request, client_secret, resource_owner_secret)
+ elif request.signature_method == SIGNATURE_HMAC_SHA512:
+ valid_signature = signature.verify_hmac_sha512(
+ request, client_secret, resource_owner_secret)
+ elif request.signature_method == SIGNATURE_PLAINTEXT:
+ valid_signature = signature.verify_plaintext(
+ request, client_secret, resource_owner_secret)
else:
- valid_signature = signature.verify_plaintext(request,
- client_secret, resource_owner_secret)
+ valid_signature = False
+
return valid_signature
diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py
index 0c22ef6..a370ccd 100644
--- a/oauthlib/oauth1/rfc5849/signature.py
+++ b/oauthlib/oauth1/rfc5849/signature.py
@@ -1,66 +1,70 @@
"""
-oauthlib.oauth1.rfc5849.signature
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This module is an implementation of `section 3.4`_ of RFC 5849.
-This module represents a direct implementation of `section 3.4`_ of the spec.
-
-Terminology:
- * Client: software interfacing with an OAuth API
- * Server: the API provider
- * Resource Owner: the user who is granting authorization to the client
+**Usage**
Steps for signing a request:
-1. Collect parameters from the uri query, auth header, & body
-2. Normalize those parameters
-3. Normalize the uri
-4. Pass the normalized uri, normalized parameters, and http method to
- construct the base string
-5. Pass the base string and any keys needed to a signing function
+1. Collect parameters from the request using ``collect_parameters``.
+2. Normalize those parameters using ``normalize_parameters``.
+3. Create the *base string URI* using ``base_string_uri``.
+4. Create the *signature base string* from the above three components
+ using ``signature_base_string``.
+5. Pass the *signature base string* and the client credentials to one of the
+ sign-with-client functions. The HMAC-based signing functions needs
+ client credentials with secrets. The RSA-based signing functions needs
+ client credentials with an RSA private key.
+
+To verify a request, pass the request and credentials to one of the verify
+functions. The HMAC-based signing functions needs the shared secrets. The
+RSA-based verify functions needs the RSA public key.
+
+**Scope**
+
+All of the functions in this module should be considered internal to OAuthLib,
+since they are not imported into the "oauthlib.oauth1" module. Programs using
+OAuthLib should not use directly invoke any of the functions in this module.
+
+**Deprecated functions**
+
+The "sign_" methods that are not "_with_client" have been deprecated. They may
+be removed in a future release. Since they are all internal functions, this
+should have no impact on properly behaving programs.
.. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
"""
+
import binascii
import hashlib
import hmac
import logging
-import urllib.parse as urlparse
+import warnings
from oauthlib.common import extract_params, safe_string_equals, urldecode
+import urllib.parse as urlparse
from . import utils
-log = logging.getLogger(__name__)
-
-def signature_base_string(http_method, base_str_uri,
- normalized_encoded_request_parameters):
- """**Construct the signature base string.**
- Per `section 3.4.1.1`_ of the spec.
+log = logging.getLogger(__name__)
- For example, the HTTP request::
- POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
- Host: example.com
- Content-Type: application/x-www-form-urlencoded
- Authorization: OAuth realm="Example",
- oauth_consumer_key="9djdj82h48djs9d2",
- oauth_token="kkk9d7dh3k39sjv7",
- oauth_signature_method="HMAC-SHA1",
- oauth_timestamp="137131201",
- oauth_nonce="7d8f3e4a",
- oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
+# ==== Common functions ==========================================
- c2&a3=2+q
+def signature_base_string(
+ http_method: str,
+ base_str_uri: str,
+ normalized_encoded_request_parameters: str) -> str:
+ """
+ Construct the signature base string.
- is represented by the following signature base string (line breaks
- are for display purposes only)::
+ The *signature base string* is the value that is calculated and signed by
+ the client. It is also independently calculated by the server to verify
+ the signature, and therefore must produce the exact same value at both
+ ends or the signature won't verify.
- POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q
- %26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_
- key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m
- ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
- 9d7dh3k39sjv7
+ The rules for calculating the *signature base string* are defined in
+ section 3.4.1.1`_ of RFC 5849.
.. _`section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
"""
@@ -91,37 +95,40 @@ def signature_base_string(http_method, base_str_uri,
# 5. The request parameters as normalized in `Section 3.4.1.3.2`_, after
# being encoded (`Section 3.6`).
#
- # .. _`Section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ # .. _`Sec 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
# .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
base_string += utils.escape(normalized_encoded_request_parameters)
return base_string
-def base_string_uri(uri, host=None):
- """**Base String URI**
- Per `section 3.4.1.2`_ of RFC 5849.
-
- For example, the HTTP request::
-
- GET /r%20v/X?id=123 HTTP/1.1
- Host: EXAMPLE.COM:80
-
- is represented by the base string URI: "http://example.com/r%20v/X".
+def base_string_uri(uri: str, host: str = None) -> str:
+ """
+ Calculates the _base string URI_.
- In another example, the HTTPS request::
+ The *base string URI* is one of the components that make up the
+ *signature base string*.
- GET /?q=1 HTTP/1.1
- Host: www.example.net:8080
+ The ``host`` is optional. If provided, it is used to override any host and
+ port values in the ``uri``. The value for ``host`` is usually extracted from
+ the "Host" request header from the HTTP request. Its value may be just the
+ hostname, or the hostname followed by a colon and a TCP/IP port number
+ (hostname:port). If a value for the``host`` is provided but it does not
+ contain a port number, the default port number is used (i.e. if the ``uri``
+ contained a port number, it will be discarded).
- is represented by the base string URI: "https://www.example.net:8080/".
+ The rules for calculating the *base string URI* are defined in
+ section 3.4.1.2`_ of RFC 5849.
.. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
- The host argument overrides the netloc part of the uri argument.
+ :param uri: URI
+ :param host: hostname with optional port number, separated by a colon
+ :return: base string URI
"""
+
if not isinstance(uri, str):
- raise ValueError('uri must be a unicode object.')
+ raise ValueError('uri must be a string.')
# FIXME: urlparse does not support unicode
scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
@@ -132,26 +139,27 @@ def base_string_uri(uri, host=None):
#
# .. _`RFC3986`: https://tools.ietf.org/html/rfc3986
- if not scheme or not netloc:
- raise ValueError('uri must include a scheme and netloc')
+ if not scheme:
+ raise ValueError('missing scheme')
# Per `RFC 2616 section 5.1.2`_:
#
# Note that the absolute path cannot be empty; if none is present in
# the original URI, it MUST be given as "/" (the server root).
#
- # .. _`RFC 2616 section 5.1.2`: https://tools.ietf.org/html/rfc2616#section-5.1.2
+ # .. _`RFC 2616 5.1.2`: https://tools.ietf.org/html/rfc2616#section-5.1.2
if not path:
path = '/'
# 1. The scheme and host MUST be in lowercase.
scheme = scheme.lower()
netloc = netloc.lower()
+ # Note: if ``host`` is used, it will be converted to lowercase below
# 2. The host and port values MUST match the content of the HTTP
# request "Host" header field.
if host is not None:
- netloc = host.lower()
+ netloc = host.lower() # override value in uri with provided host
# 3. The port MUST be included if it is not the default port for the
# scheme, and MUST be excluded if it is the default. Specifically,
@@ -161,14 +169,34 @@ def base_string_uri(uri, host=None):
#
# .. _`RFC2616`: https://tools.ietf.org/html/rfc2616
# .. _`RFC2818`: https://tools.ietf.org/html/rfc2818
- default_ports = (
- ('http', '80'),
- ('https', '443'),
- )
+
if ':' in netloc:
- host, port = netloc.split(':', 1)
- if (scheme, port) in default_ports:
- netloc = host
+ # Contains a colon ":", so try to parse as "host:port"
+
+ hostname, port_str = netloc.split(':', 1)
+
+ if len(hostname) == 0:
+ raise ValueError('missing host') # error: netloc was ":port" or ":"
+
+ if len(port_str) == 0:
+ netloc = hostname # was "host:", so just use the host part
+ else:
+ try:
+ port_num = int(port_str) # try to parse into an integer number
+ except ValueError:
+ raise ValueError('port is not an integer')
+
+ if port_num <= 0 or 65535 < port_num:
+ raise ValueError('port out of range') # 16-bit unsigned ints
+ if (scheme, port_num) in (('http', 80), ('https', 443)):
+ netloc = hostname # default port for scheme: exclude port num
+ else:
+ netloc = hostname + ':' + str(port_num) # use hostname:port
+ else:
+ # Does not contain a colon, so entire value must be the hostname
+
+ if len(netloc) == 0:
+ raise ValueError('missing host') # error: netloc was empty string
v = urlparse.urlunparse((scheme, netloc, path, params, '', ''))
@@ -197,77 +225,29 @@ def base_string_uri(uri, host=None):
return v.replace(' ', '%20')
-# ** Request Parameters **
-#
-# Per `section 3.4.1.3`_ of the spec.
-#
-# In order to guarantee a consistent and reproducible representation of
-# the request parameters, the parameters are collected and decoded to
-# their original decoded form. They are then sorted and encoded in a
-# particular manner that is often different from their original
-# encoding scheme, and concatenated into a single string.
-#
-# .. _`section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3
-
-def collect_parameters(uri_query='', body=[], headers=None,
+def collect_parameters(uri_query='', body=None, headers=None,
exclude_oauth_signature=True, with_realm=False):
- """**Parameter Sources**
+ """
+ Gather the request parameters from all the parameter sources.
+
+ This function is used to extract all the parameters, which are then passed
+ to ``normalize_parameters`` to produce one of the components that make up
+ the *signature base string*.
Parameters starting with `oauth_` will be unescaped.
Body parameters must be supplied as a dict, a list of 2-tuples, or a
- formencoded query string.
+ form encoded query string.
Headers must be supplied as a dict.
- Per `section 3.4.1.3.1`_ of the spec.
-
- For example, the HTTP request::
-
- POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
- Host: example.com
- Content-Type: application/x-www-form-urlencoded
- Authorization: OAuth realm="Example",
- oauth_consumer_key="9djdj82h48djs9d2",
- oauth_token="kkk9d7dh3k39sjv7",
- oauth_signature_method="HMAC-SHA1",
- oauth_timestamp="137131201",
- oauth_nonce="7d8f3e4a",
- oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"
-
- c2&a3=2+q
-
- contains the following (fully decoded) parameters used in the
- signature base sting::
-
- +------------------------+------------------+
- | Name | Value |
- +------------------------+------------------+
- | b5 | =%3D |
- | a3 | a |
- | c@ | |
- | a2 | r b |
- | oauth_consumer_key | 9djdj82h48djs9d2 |
- | oauth_token | kkk9d7dh3k39sjv7 |
- | oauth_signature_method | HMAC-SHA1 |
- | oauth_timestamp | 137131201 |
- | oauth_nonce | 7d8f3e4a |
- | c2 | |
- | a3 | 2 q |
- +------------------------+------------------+
-
- Note that the value of "b5" is "=%3D" and not "==". Both "c@" and
- "c2" have empty values. While the encoding rules specified in this
- specification for the purpose of constructing the signature base
- string exclude the use of a "+" character (ASCII code 43) to
- represent an encoded space character (ASCII code 32), this practice
- is widely used in "application/x-www-form-urlencoded" encoded values,
- and MUST be properly decoded, as demonstrated by one of the "a3"
- parameter instances (the "a3" parameter is used twice in this
- request).
-
- .. _`section 3.4.1.3.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+ The rules where the parameters must be sourced from are defined in
+ `section 3.4.1.3.1`_ of RFC 5849.
+
+ .. _`Sec 3.4.1.3.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
"""
+ if body is None:
+ body = []
headers = headers or {}
params = []
@@ -278,11 +258,11 @@ def collect_parameters(uri_query='', body=[], headers=None,
# `RFC3986, Section 3.4`_. The query component is parsed into a list
# of name/value pairs by treating it as an
# "application/x-www-form-urlencoded" string, separating the names
- # and values and decoding them as defined by
- # `W3C.REC-html40-19980424`_, Section 17.13.4.
+ # and values and decoding them as defined by W3C.REC-html40-19980424
+ # `W3C-HTML-4.0`_, Section 17.13.4.
#
- # .. _`RFC3986, Section 3.4`: https://tools.ietf.org/html/rfc3986#section-3.4
- # .. _`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+ # .. _`RFC3986, Sec 3.4`: https://tools.ietf.org/html/rfc3986#section-3.4
+ # .. _`W3C-HTML-4.0`: https://www.w3.org/TR/1998/REC-html40-19980424/
if uri_query:
params.extend(urldecode(uri_query))
@@ -305,12 +285,12 @@ def collect_parameters(uri_query='', body=[], headers=None,
#
# * The entity-body follows the encoding requirements of the
# "application/x-www-form-urlencoded" content-type as defined by
- # `W3C.REC-html40-19980424`_.
+ # W3C.REC-html40-19980424 `W3C-HTML-4.0`_.
# * The HTTP request entity-header includes the "Content-Type"
# header field set to "application/x-www-form-urlencoded".
#
- # .._`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+ # .. _`W3C-HTML-4.0`: https://www.w3.org/TR/1998/REC-html40-19980424/
# TODO: enforce header param inclusion conditions
bodyparams = extract_params(body) or []
@@ -332,75 +312,17 @@ def collect_parameters(uri_query='', body=[], headers=None,
return unescaped_params
-def normalize_parameters(params):
- """**Parameters Normalization**
- Per `section 3.4.1.3.2`_ of the spec.
-
- For example, the list of parameters from the previous section would
- be normalized as follows:
-
- Encoded::
-
- +------------------------+------------------+
- | Name | Value |
- +------------------------+------------------+
- | b5 | %3D%253D |
- | a3 | a |
- | c%40 | |
- | a2 | r%20b |
- | oauth_consumer_key | 9djdj82h48djs9d2 |
- | oauth_token | kkk9d7dh3k39sjv7 |
- | oauth_signature_method | HMAC-SHA1 |
- | oauth_timestamp | 137131201 |
- | oauth_nonce | 7d8f3e4a |
- | c2 | |
- | a3 | 2%20q |
- +------------------------+------------------+
-
- Sorted::
-
- +------------------------+------------------+
- | Name | Value |
- +------------------------+------------------+
- | a2 | r%20b |
- | a3 | 2%20q |
- | a3 | a |
- | b5 | %3D%253D |
- | c%40 | |
- | c2 | |
- | oauth_consumer_key | 9djdj82h48djs9d2 |
- | oauth_nonce | 7d8f3e4a |
- | oauth_signature_method | HMAC-SHA1 |
- | oauth_timestamp | 137131201 |
- | oauth_token | kkk9d7dh3k39sjv7 |
- +------------------------+------------------+
-
- Concatenated Pairs::
-
- +-------------------------------------+
- | Name=Value |
- +-------------------------------------+
- | a2=r%20b |
- | a3=2%20q |
- | a3=a |
- | b5=%3D%253D |
- | c%40= |
- | c2= |
- | oauth_consumer_key=9djdj82h48djs9d2 |
- | oauth_nonce=7d8f3e4a |
- | oauth_signature_method=HMAC-SHA1 |
- | oauth_timestamp=137131201 |
- | oauth_token=kkk9d7dh3k39sjv7 |
- +-------------------------------------+
-
- and concatenated together into a single string (line breaks are for
- display purposes only)::
-
- a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj
- dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1
- &oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7
-
- .. _`section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+def normalize_parameters(params) -> str:
+ """
+ Calculate the normalized request parameters.
+
+ The *normalized request parameters* is one of the components that make up
+ the *signature base string*.
+
+ The rules for parameter normalization are defined in `section 3.4.1.3.2`_ of
+ RFC 5849.
+
+ .. _`Sec 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
"""
# The parameters collected in `Section 3.4.1.3`_ are normalized into a
@@ -430,34 +352,33 @@ def normalize_parameters(params):
return '&'.join(parameter_parts)
-def sign_hmac_sha1_with_client(base_string, client):
- return sign_hmac_sha1(base_string,
- client.client_secret,
- client.resource_owner_secret
- )
+# ==== Common functions for HMAC-based signature methods =========
+def _sign_hmac(hash_algorithm_name: str,
+ sig_base_str: str,
+ client_secret: str,
+ resource_owner_secret: str):
+ """
+ **HMAC-SHA256**
-def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
- """**HMAC-SHA1**
-
- The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature
- algorithm as defined in `RFC2104`_::
+ The "HMAC-SHA256" signature method uses the HMAC-SHA256 signature
+ algorithm as defined in `RFC4634`_::
- digest = HMAC-SHA1 (key, text)
+ digest = HMAC-SHA256 (key, text)
Per `section 3.4.2`_ of the spec.
- .. _`RFC2104`: https://tools.ietf.org/html/rfc2104
+ .. _`RFC4634`: https://tools.ietf.org/html/rfc4634
.. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2
"""
- # The HMAC-SHA1 function variables are used in following way:
+ # The HMAC-SHA256 function variables are used in following way:
# text is set to the value of the signature base string from
# `Section 3.4.1.1`_.
#
# .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
- text = base_string
+ text = sig_base_str
# key is set to the concatenated values of:
# 1. The client shared-secret, after being encoded (`Section 3.6`_).
@@ -474,251 +395,438 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
# .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
key += utils.escape(resource_owner_secret or '')
+ # Get the hashing algorithm to use
+
+ m = {
+ 'SHA-1': hashlib.sha1,
+ 'SHA-256': hashlib.sha256,
+ 'SHA-512': hashlib.sha512,
+ }
+ hash_alg = m[hash_algorithm_name]
+
+ # Calculate the signature
+
# FIXME: HMAC does not support unicode!
key_utf8 = key.encode('utf-8')
text_utf8 = text.encode('utf-8')
- signature = hmac.new(key_utf8, text_utf8, hashlib.sha1)
+ signature = hmac.new(key_utf8, text_utf8, hash_alg)
# digest is used to set the value of the "oauth_signature" protocol
# parameter, after the result octet string is base64-encoded
# per `RFC2045, Section 6.8`.
#
- # .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
+ # .. _`RFC2045, Sec 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
-def sign_hmac_sha256_with_client(base_string, client):
- return sign_hmac_sha256(base_string,
- client.client_secret,
- client.resource_owner_secret
- )
+def _verify_hmac(hash_algorithm_name: str,
+ request,
+ client_secret=None,
+ resource_owner_secret=None):
+ """Verify a HMAC-SHA1 signature.
+
+ Per `section 3.4`_ of the spec.
+
+ .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
+
+ To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
+ attribute MUST be an absolute URI whose netloc part identifies the
+ origin server or gateway on which the resource resides. Any Host
+ item of the request argument's headers dict attribute will be
+ ignored.
+
+ .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
+
+ """
+ norm_params = normalize_parameters(request.params)
+ bs_uri = base_string_uri(request.uri)
+ sig_base_str = signature_base_string(request.http_method, bs_uri,
+ norm_params)
+ signature = _sign_hmac(hash_algorithm_name, sig_base_str,
+ client_secret, resource_owner_secret)
+ match = safe_string_equals(signature, request.signature)
+ if not match:
+ log.debug('Verify HMAC failed: signature base string: %s', sig_base_str)
+ return match
+
+
+# ==== HMAC-SHA1 =================================================
+
+def sign_hmac_sha1_with_client(sig_base_str, client):
+ return _sign_hmac('SHA-1', sig_base_str,
+ client.client_secret, client.resource_owner_secret)
+
+
+def verify_hmac_sha1(request, client_secret=None, resource_owner_secret=None):
+ return _verify_hmac('SHA-1', request, client_secret, resource_owner_secret)
+
+
+def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
+ """
+ Deprecated function for calculating a HMAC-SHA1 signature.
+
+ This function has been replaced by invoking ``sign_hmac`` with "SHA-1"
+ as the hash algorithm name.
+
+ This function was invoked by sign_hmac_sha1_with_client and
+ test_signatures.py, but does any application invoke it directly? If not,
+ it can be removed.
+ """
+ warnings.warn('use sign_hmac_sha1_with_client instead of sign_hmac_sha1',
+ DeprecationWarning)
+
+ # For some unknown reason, the original implementation assumed base_string
+ # could either be bytes or str. The signature base string calculating
+ # function always returned a str, so the new ``sign_rsa`` only expects that.
+
+ base_string = base_string.decode('ascii') \
+ if isinstance(base_string, bytes) else base_string
+
+ return _sign_hmac('SHA-1', base_string,
+ client_secret, resource_owner_secret)
+
+
+# ==== HMAC-SHA256 ===============================================
+
+def sign_hmac_sha256_with_client(sig_base_str, client):
+ return _sign_hmac('SHA-256', sig_base_str,
+ client.client_secret, client.resource_owner_secret)
+
+
+def verify_hmac_sha256(request, client_secret=None, resource_owner_secret=None):
+ return _verify_hmac('SHA-256', request,
+ client_secret, resource_owner_secret)
def sign_hmac_sha256(base_string, client_secret, resource_owner_secret):
- """**HMAC-SHA256**
+ """
+ Deprecated function for calculating a HMAC-SHA256 signature.
- The "HMAC-SHA256" signature method uses the HMAC-SHA256 signature
- algorithm as defined in `RFC4634`_::
+ This function has been replaced by invoking ``sign_hmac`` with "SHA-256"
+ as the hash algorithm name.
- digest = HMAC-SHA256 (key, text)
+ This function was invoked by sign_hmac_sha256_with_client and
+ test_signatures.py, but does any application invoke it directly? If not,
+ it can be removed.
+ """
+ warnings.warn(
+ 'use sign_hmac_sha256_with_client instead of sign_hmac_sha256',
+ DeprecationWarning)
- Per `section 3.4.2`_ of the spec.
+ # For some unknown reason, the original implementation assumed base_string
+ # could either be bytes or str. The signature base string calculating
+ # function always returned a str, so the new ``sign_rsa`` only expects that.
- .. _`RFC4634`: https://tools.ietf.org/html/rfc4634
- .. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2
+ base_string = base_string.decode('ascii') \
+ if isinstance(base_string, bytes) else base_string
+
+ return _sign_hmac('SHA-256', base_string,
+ client_secret, resource_owner_secret)
+
+
+# ==== HMAC-SHA512 ===============================================
+
+def sign_hmac_sha512_with_client(sig_base_str: str,
+ client):
+ return _sign_hmac('SHA-512', sig_base_str,
+ client.client_secret, client.resource_owner_secret)
+
+
+def verify_hmac_sha512(request,
+ client_secret: str = None,
+ resource_owner_secret: str = None):
+ return _verify_hmac('SHA-512', request,
+ client_secret, resource_owner_secret)
+
+
+# ==== Common functions for RSA-based signature methods ==========
+
+_jwt_rsa = {} # cache of RSA-hash implementations from PyJWT jwt.algorithms
+
+
+def _get_jwt_rsa_algorithm(hash_algorithm_name: str):
"""
+ Obtains an RSAAlgorithm object that implements RSA with the hash algorithm.
- # The HMAC-SHA256 function variables are used in following way:
+ This method maintains the ``_jwt_rsa`` cache.
- # text is set to the value of the signature base string from
- # `Section 3.4.1.1`_.
- #
- # .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
- text = base_string
+ Returns a jwt.algorithm.RSAAlgorithm.
+ """
+ if hash_algorithm_name in _jwt_rsa:
+ # Found in cache: return it
+ return _jwt_rsa[hash_algorithm_name]
+ else:
+ # Not in cache: instantiate a new RSAAlgorithm
- # key is set to the concatenated values of:
- # 1. The client shared-secret, after being encoded (`Section 3.6`_).
- #
- # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
- key = utils.escape(client_secret or '')
+ # PyJWT has some nice pycrypto/cryptography abstractions
+ import jwt.algorithms as jwt_algorithms
+ m = {
+ 'SHA-1': jwt_algorithms.hashes.SHA1,
+ 'SHA-256': jwt_algorithms.hashes.SHA256,
+ 'SHA-512': jwt_algorithms.hashes.SHA512,
+ }
+ v = jwt_algorithms.RSAAlgorithm(m[hash_algorithm_name])
- # 2. An "&" character (ASCII code 38), which MUST be included
- # even when either secret is empty.
- key += '&'
+ _jwt_rsa[hash_algorithm_name] = v # populate cache
- # 3. The token shared-secret, after being encoded (`Section 3.6`_).
- #
- # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
- key += utils.escape(resource_owner_secret or '')
+ return v
- # FIXME: HMAC does not support unicode!
- key_utf8 = key.encode('utf-8')
- text_utf8 = text.encode('utf-8')
- signature = hmac.new(key_utf8, text_utf8, hashlib.sha256)
- # digest is used to set the value of the "oauth_signature" protocol
- # parameter, after the result octet string is base64-encoded
- # per `RFC2045, Section 6.8`.
- #
- # .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
- return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
+def _prepare_key_plus(alg, keystr):
+ """
+ Prepare a PEM encoded key (public or private), by invoking the `prepare_key`
+ method on alg with the keystr.
+
+ The keystr should be a string or bytes. If the keystr is bytes, it is
+ decoded as UTF-8 before being passed to prepare_key. Otherwise, it
+ is passed directly.
+ """
+ if isinstance(keystr, bytes):
+ keystr = keystr.decode('utf-8')
+ return alg.prepare_key(keystr)
-_jwtrs1 = None
-#jwt has some nice pycrypto/cryptography abstractions
-def _jwt_rs1_signing_algorithm():
- global _jwtrs1
- if _jwtrs1 is None:
- import jwt.algorithms as jwtalgo
- _jwtrs1 = jwtalgo.RSAAlgorithm(jwtalgo.hashes.SHA1)
- return _jwtrs1
+def _sign_rsa(hash_algorithm_name: str,
+ sig_base_str: str,
+ rsa_private_key: str):
+ """
+ Calculate the signature for an RSA-based signature method.
-def sign_rsa_sha1(base_string, rsa_private_key):
- """**RSA-SHA1**
+ The ``alg`` is used to calculate the digest over the signature base string.
+ For the "RSA_SHA1" signature method, the alg must be SHA-1. While OAuth 1.0a
+ only defines the RSA-SHA1 signature method, this function can be used for
+ other non-standard signature methods that only differ from RSA-SHA1 by the
+ digest algorithm.
- Per `section 3.4.3`_ of the spec.
+ Signing for the RSA-SHA1 signature method is defined in
+ `section 3.4.3`_ of RFC 5849.
- The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature
- algorithm as defined in `RFC3447, Section 8.2`_ (also known as
- PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5. To
+ The RSASSA-PKCS1-v1_5 signature algorithm used defined by
+ `RFC3447, Section 8.2`_ (also known as PKCS#1), with the `alg` as the
+ hash function for EMSA-PKCS1-v1_5. To
use this method, the client MUST have established client credentials
with the server that included its RSA public key (in a manner that is
beyond the scope of this specification).
.. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3
.. _`RFC3447, Section 8.2`: https://tools.ietf.org/html/rfc3447#section-8.2
-
"""
- if isinstance(base_string, str):
- base_string = base_string.encode('utf-8')
- # TODO: finish RSA documentation
- alg = _jwt_rs1_signing_algorithm()
+
+ # Get the implementation of RSA-hash
+
+ alg = _get_jwt_rsa_algorithm(hash_algorithm_name)
+
+ # Check private key
+
+ if not rsa_private_key:
+ raise ValueError('rsa_private_key required for RSA with ' +
+ alg.hash_alg.name + ' signature method')
+
+ # Convert the "signature base string" into a sequence of bytes (M)
+ #
+ # The signature base string, by definition, only contain printable US-ASCII
+ # characters. So encoding it as 'ascii' will always work. It will raise a
+ # ``UnicodeError`` if it can't encode the value, which will never happen
+ # if the signature base string was created correctly. Therefore, using
+ # 'ascii' encoding provides an extra level of error checking.
+
+ m = sig_base_str.encode('ascii')
+
+ # Perform signing: S = RSASSA-PKCS1-V1_5-SIGN (K, M)
+
key = _prepare_key_plus(alg, rsa_private_key)
- s=alg.sign(base_string, key)
- return binascii.b2a_base64(s)[:-1].decode('utf-8')
+ s = alg.sign(m, key)
+
+ # base64-encoded per RFC2045 section 6.8.
+ #
+ # 1. While b2a_base64 implements base64 defined by RFC 3548. As used here,
+ # it is the same as base64 defined by RFC 2045.
+ # 2. b2a_base64 includes a "\n" at the end of its result ([:-1] removes it)
+ # 3. b2a_base64 produces a binary string. Use decode to produce a str.
+ # It should only contain only printable US-ASCII characters.
+ return binascii.b2a_base64(s)[:-1].decode('ascii')
-def sign_rsa_sha1_with_client(base_string, client):
- if not client.rsa_key:
- raise ValueError('rsa_key is required when using RSA signature method.')
- return sign_rsa_sha1(base_string, client.rsa_key)
+def _verify_rsa(hash_algorithm_name: str,
+ request,
+ rsa_public_key: str):
+ """
+ Verify a base64 encoded signature for a RSA-based signature method.
-def sign_plaintext(client_secret, resource_owner_secret):
- """Sign a request using plaintext.
+ The ``alg`` is used to calculate the digest over the signature base string.
+ For the "RSA_SHA1" signature method, the alg must be SHA-1. While OAuth 1.0a
+ only defines the RSA-SHA1 signature method, this function can be used for
+ other non-standard signature methods that only differ from RSA-SHA1 by the
+ digest algorithm.
- Per `section 3.4.4`_ of the spec.
+ Verification for the RSA-SHA1 signature method is defined in
+ `section 3.4.3`_ of RFC 5849.
- The "PLAINTEXT" method does not employ a signature algorithm. It
- MUST be used with a transport-layer mechanism such as TLS or SSL (or
- sent over a secure channel with equivalent protections). It does not
- utilize the signature base string or the "oauth_timestamp" and
- "oauth_nonce" parameters.
+ .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3
- .. _`section 3.4.4`: https://tools.ietf.org/html/rfc5849#section-3.4.4
+ To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
+ attribute MUST be an absolute URI whose netloc part identifies the
+ origin server or gateway on which the resource resides. Any Host
+ item of the request argument's headers dict attribute will be
+ ignored.
+ .. _`RFC2616 Sec 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
"""
- # The "oauth_signature" protocol parameter is set to the concatenated
- # value of:
+ try:
+ # Calculate the *signature base string* of the actual received request
- # 1. The client shared-secret, after being encoded (`Section 3.6`_).
- #
- # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
- signature = utils.escape(client_secret or '')
+ norm_params = normalize_parameters(request.params)
+ bs_uri = base_string_uri(request.uri)
+ sig_base_str = signature_base_string(
+ request.http_method, bs_uri, norm_params)
- # 2. An "&" character (ASCII code 38), which MUST be included even
- # when either secret is empty.
- signature += '&'
+ # Obtain the signature that was received in the request
- # 3. The token shared-secret, after being encoded (`Section 3.6`_).
- #
- # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
- signature += utils.escape(resource_owner_secret or '')
+ sig = binascii.a2b_base64(request.signature.encode('ascii'))
- return signature
+ # Get the implementation of RSA-with-hash algorithm to use
+ alg = _get_jwt_rsa_algorithm(hash_algorithm_name)
-def sign_plaintext_with_client(base_string, client):
- return sign_plaintext(client.client_secret, client.resource_owner_secret)
+ # Verify the received signature was produced by the private key
+ # corresponding to the `rsa_public_key`, signing exact same
+ # *signature base string*.
+ #
+ # RSASSA-PKCS1-V1_5-VERIFY ((n, e), M, S)
+ key = _prepare_key_plus(alg, rsa_public_key)
-def verify_hmac_sha1(request, client_secret=None,
- resource_owner_secret=None):
- """Verify a HMAC-SHA1 signature.
+ # The signature base string only contain printable US-ASCII characters.
+ # The ``encode`` method with the default "strict" error handling will
+ # raise a ``UnicodeError`` if it can't encode the value. So using
+ # "ascii" will always work.
- Per `section 3.4`_ of the spec.
+ verify_ok = alg.verify(sig_base_str.encode('ascii'), key, sig)
- .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
+ if not verify_ok:
+ log.debug('Verify failed: RSA with ' + alg.hash_alg.name +
+ ': signature base string=%s' + sig_base_str)
+ return verify_ok
- To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
- attribute MUST be an absolute URI whose netloc part identifies the
- origin server or gateway on which the resource resides. Any Host
- item of the request argument's headers dict attribute will be
- ignored.
+ except UnicodeError:
+ # A properly encoded signature will only contain printable US-ASCII
+ # characters. The ``encode`` method with the default "strict" error
+ # handling will raise a ``UnicodeError`` if it can't decode the value.
+ # So using "ascii" will work with all valid signatures. But an
+ # incorrectly or maliciously produced signature could contain other
+ # bytes.
+ #
+ # This implementation treats that situation as equivalent to the
+ # signature verification having failed.
+ #
+ # Note: simply changing the encode to use 'utf-8' will not remove this
+ # case, since an incorrect or malicious request can contain bytes which
+ # are invalid as UTF-8.
+ return False
- .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
- """
- norm_params = normalize_parameters(request.params)
- bs_uri = base_string_uri(request.uri)
- sig_base_str = signature_base_string(request.http_method, bs_uri,
- norm_params)
- signature = sign_hmac_sha1(sig_base_str, client_secret,
- resource_owner_secret)
- match = safe_string_equals(signature, request.signature)
- if not match:
- log.debug('Verify HMAC-SHA1 failed: signature base string: %s',
- sig_base_str)
- return match
+# ==== RSA-SHA1 ==================================================
+def sign_rsa_sha1_with_client(sig_base_str, client):
+ # For some reason, this function originally accepts both str and bytes.
+ # This behaviour is preserved here. But won't be done for the newer
+ # sign_rsa_sha256_with_client and sign_rsa_sha512_with_client functions,
+ # which will only accept strings. The function to calculate a
+ # "signature base string" always produces a string, so it is not clear
+ # why support for bytes would ever be needed.
+ sig_base_str = sig_base_str.decode('ascii')\
+ if isinstance(sig_base_str, bytes) else sig_base_str
-def verify_hmac_sha256(request, client_secret=None,
- resource_owner_secret=None):
- """Verify a HMAC-SHA256 signature.
+ return _sign_rsa('SHA-1', sig_base_str, client.rsa_key)
- Per `section 3.4`_ of the spec.
- .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
+def verify_rsa_sha1(request, rsa_public_key: str):
+ return _verify_rsa('SHA-1', request, rsa_public_key)
- To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
- attribute MUST be an absolute URI whose netloc part identifies the
- origin server or gateway on which the resource resides. Any Host
- item of the request argument's headers dict attribute will be
- ignored.
- .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
+def sign_rsa_sha1(base_string, rsa_private_key):
+ """
+ Deprecated function for calculating a RSA-SHA1 signature.
+
+ This function has been replaced by invoking ``sign_rsa`` with "SHA-1"
+ as the hash algorithm name.
+ This function was invoked by sign_rsa_sha1_with_client and
+ test_signatures.py, but does any application invoke it directly? If not,
+ it can be removed.
"""
- norm_params = normalize_parameters(request.params)
- bs_uri = base_string_uri(request.uri)
- sig_base_str = signature_base_string(request.http_method, bs_uri,
- norm_params)
- signature = sign_hmac_sha256(sig_base_str, client_secret,
- resource_owner_secret)
- match = safe_string_equals(signature, request.signature)
- if not match:
- log.debug('Verify HMAC-SHA256 failed: signature base string: %s',
- sig_base_str)
- return match
+ warnings.warn('use _sign_rsa("SHA-1", ...) instead of sign_rsa_sha1',
+ DeprecationWarning)
+ if isinstance(base_string, bytes):
+ base_string = base_string.decode('ascii')
-def _prepare_key_plus(alg, keystr):
- if isinstance(keystr, bytes):
- keystr = keystr.decode('utf-8')
- return alg.prepare_key(keystr)
+ return _sign_rsa('SHA-1', base_string, rsa_private_key)
-def verify_rsa_sha1(request, rsa_public_key):
- """Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature.
- Per `section 3.4.3`_ of the spec.
+# ==== RSA-SHA256 ================================================
- Note this method requires the jwt and cryptography libraries.
+def sign_rsa_sha256_with_client(sig_base_str: str, client):
+ return _sign_rsa('SHA-256', sig_base_str, client.rsa_key)
- .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3
- To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
- attribute MUST be an absolute URI whose netloc part identifies the
- origin server or gateway on which the resource resides. Any Host
- item of the request argument's headers dict attribute will be
- ignored.
+def verify_rsa_sha256(request, rsa_public_key: str):
+ return _verify_rsa('SHA-256', request, rsa_public_key)
+
+
+# ==== RSA-SHA512 ================================================
+
+def sign_rsa_sha512_with_client(sig_base_str: str, client):
+ return _sign_rsa('SHA-512', sig_base_str, client.rsa_key)
+
+
+def verify_rsa_sha512(request, rsa_public_key: str):
+ return _verify_rsa('SHA-512', request, rsa_public_key)
+
+
+# ==== PLAINTEXT =================================================
+
+def sign_plaintext_with_client(_signature_base_string, client):
+ # _signature_base_string is not used because the signature with PLAINTEXT
+ # is just the secret: it isn't a real signature.
+ return sign_plaintext(client.client_secret, client.resource_owner_secret)
+
+
+def sign_plaintext(client_secret, resource_owner_secret):
+ """Sign a request using plaintext.
+
+ Per `section 3.4.4`_ of the spec.
+
+ The "PLAINTEXT" method does not employ a signature algorithm. It
+ MUST be used with a transport-layer mechanism such as TLS or SSL (or
+ sent over a secure channel with equivalent protections). It does not
+ utilize the signature base string or the "oauth_timestamp" and
+ "oauth_nonce" parameters.
+
+ .. _`section 3.4.4`: https://tools.ietf.org/html/rfc5849#section-3.4.4
- .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
"""
- norm_params = normalize_parameters(request.params)
- bs_uri = base_string_uri(request.uri)
- sig_base_str = signature_base_string(request.http_method, bs_uri,
- norm_params).encode('utf-8')
- sig = binascii.a2b_base64(request.signature.encode('utf-8'))
- alg = _jwt_rs1_signing_algorithm()
- key = _prepare_key_plus(alg, rsa_public_key)
+ # The "oauth_signature" protocol parameter is set to the concatenated
+ # value of:
+
+ # 1. The client shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ signature = utils.escape(client_secret or '')
+
+ # 2. An "&" character (ASCII code 38), which MUST be included even
+ # when either secret is empty.
+ signature += '&'
+
+ # 3. The token shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ signature += utils.escape(resource_owner_secret or '')
- verify_ok = alg.verify(sig_base_str, key, sig)
- if not verify_ok:
- log.debug('Verify RSA-SHA1 failed: signature base string: %s',
- sig_base_str)
- return verify_ok
+ return signature
def verify_plaintext(request, client_secret=None, resource_owner_secret=None):
diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py
index d30bfd7..81ee1de 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py
@@ -161,10 +161,10 @@ class MetadataEndpoint(BaseEndpoint):
response_types_supported
REQUIRED.
- * Other OPTIONAL fields:
- jwks_uri
- registration_endpoint
- response_modes_supported
+ Other OPTIONAL fields:
+ jwks_uri,
+ registration_endpoint,
+ response_modes_supported
grant_types_supported
OPTIONAL. JSON array containing a list of the OAuth 2.0 grant
diff --git a/setup.py b/setup.py
index 6fada45..0babb45 100755
--- a/setup.py
+++ b/setup.py
@@ -16,9 +16,9 @@ def fread(fn):
return f.read()
-rsa_require = ['cryptography']
-signedtoken_require = ['cryptography', 'pyjwt>=1.0.0']
-signals_require = ['blinker']
+rsa_require = ['cryptography>=1.4.0']
+signedtoken_require = ['cryptography>=1.4.0', 'pyjwt>=1.6.0']
+signals_require = ['blinker>=1.4.0']
setup(
name='oauthlib',
diff --git a/tests/oauth1/rfc5849/test_signatures.py b/tests/oauth1/rfc5849/test_signatures.py
index 2de4e8a..3e84f24 100644
--- a/tests/oauth1/rfc5849/test_signatures.py
+++ b/tests/oauth1/rfc5849/test_signatures.py
@@ -1,301 +1,588 @@
# -*- coding: utf-8 -*-
-from urllib.parse import quote
-
from oauthlib.oauth1.rfc5849.signature import (
- base_string_uri, collect_parameters, normalize_parameters, sign_hmac_sha1,
- sign_hmac_sha1_with_client, sign_plaintext, sign_plaintext_with_client,
- sign_rsa_sha1, sign_rsa_sha1_with_client, signature_base_string,
+ collect_parameters,
+ signature_base_string,
+ base_string_uri,
+ normalize_parameters,
+ sign_hmac_sha1_with_client,
+ sign_hmac_sha256_with_client,
+ sign_hmac_sha512_with_client,
+ sign_rsa_sha1_with_client,
+ sign_rsa_sha256_with_client,
+ sign_rsa_sha512_with_client,
+ sign_plaintext_with_client,
+ verify_hmac_sha1,
+ verify_hmac_sha256,
+ verify_hmac_sha512,
+ verify_rsa_sha1,
+ verify_rsa_sha256,
+ verify_rsa_sha512,
+ verify_plaintext
)
-
from tests.unittest import TestCase
+# ################################################################
+
+class MockRequest:
+ """
+ Mock of a request used by the verify_* functions.
+ """
+
+ def __init__(self,
+ method: str,
+ uri_str: str,
+ params: list,
+ signature: str):
+ """
+ The params is a list of (name, value) tuples. It is not a dictionary,
+ because there can be multiple parameters with the same name.
+ """
+ self.uri = uri_str
+ self.http_method = method
+ self.params = params
+ self.signature = signature
+
+
+# ################################################################
+
+class MockClient:
+ """
+ Mock of client credentials used by the sign_*_with_client functions.
+
+ For HMAC, set the client_secret and resource_owner_secret.
+
+ For RSA, set the rsa_key to either a PEM formatted PKCS #1 public key or
+ PEM formatted PKCS #1 private key.
+ """
+ def __init__(self,
+ client_secret: str = None,
+ resource_owner_secret: str = None,
+ rsa_key: str = None):
+ self.client_secret = client_secret
+ self.resource_owner_secret = resource_owner_secret
+ self.rsa_key = rsa_key # used for private or public key: a poor design!
+
+
+# ################################################################
+
class SignatureTests(TestCase):
- class MockClient(dict):
- def __getattr__(self, name):
- return self[name]
-
- def __setattr__(self, name, value):
- self[name] = value
-
- def decode(self):
- for k, v in self.items():
- self[k] = v.decode('utf-8')
-
- uri_query = "b5=%3D%253D&a3=a&c%40=&a2=r%20b"
- authorization_header = """OAuth realm="Example",
- oauth_consumer_key="9djdj82h48djs9d2",
- oauth_token="kkk9d7dh3k39sjv7",
- oauth_signature_method="HMAC-SHA1",
- oauth_timestamp="137131201",
- oauth_nonce="7d8f3e4a",
- oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D" """.strip()
- body = "c2&a3=2+q"
- http_method = b"post"
- base_string_url = (
- "http://example.com/request?{}".format(uri_query)).encode('utf-8')
- unnormalized_request_parameters =[
- ('OAuth realm',"Example"),
- ('oauth_consumer_key',"9djdj82h48djs9d2"),
- ('oauth_token',"kkk9d7dh3k39sjv7"),
- ('oauth_signature_method',"HMAC-SHA1"),
- ('oauth_timestamp',"137131201"),
- ('oauth_nonce',"7d8f3e4a"),
- ('oauth_signature',"bYT5CMsGcbgUdFHObYMEfcx6bsw%3D")
+ """
+ Unit tests for the oauthlib/oauth1/rfc5849/signature.py module.
+
+ The tests in this class are organised into sections, to test the
+ functions relating to:
+
+ - Signature base string calculation
+ - HMAC-based signature methods
+ - RSA-based signature methods
+ - PLAINTEXT signature method
+
+ Each section is separated by a comment beginning with "====".
+
+ Those comments have been formatted to remain visible when the code is
+ collapsed using PyCharm's code folding feature. That is, those section
+ heading comments do not have any other comment lines around it, so they
+ don't get collapsed when the contents of the class is collapsed. While
+ there is a "Sequential comments" option in the code folding configuration,
+ by default they are folded.
+
+ They all use some/all of the example test vector, defined in the first
+ section below.
+ """
+
+ # ==== Example test vector =======================================
+
+ eg_signature_base_string =\
+ 'POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q' \
+ '%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_' \
+ 'key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m' \
+ 'ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk' \
+ '9d7dh3k39sjv7'
+
+ # The _signature base string_ above is copied from the end of
+ # RFC 5849 section 3.4.1.1.
+ #
+ # It corresponds to the three values below.
+ #
+ # The _normalized parameters_ below is copied from the end of
+ # RFC 5849 section 3.4.1.3.2.
+
+ eg_http_method = 'POST'
+
+ eg_base_string_uri = 'http://example.com/request'
+
+ eg_normalized_parameters =\
+ 'a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj' \
+ 'dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1' \
+ '&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7'
+
+ # The above _normalized parameters_ corresponds to the parameters below.
+ #
+ # The parameters below is copied from the table at the end of
+ # RFC 5849 section 3.4.1.3.1.
+
+ eg_params = [
+ ('b5', '=%3D'),
+ ('a3', 'a'),
+ ('c@', ''),
+ ('a2', 'r b'),
+ ('oauth_consumer_key', '9djdj82h48djs9d2'),
+ ('oauth_token', 'kkk9d7dh3k39sjv7'),
+ ('oauth_signature_method', 'HMAC-SHA1'),
+ ('oauth_timestamp', '137131201'),
+ ('oauth_nonce', '7d8f3e4a'),
+ ('c2', ''),
+ ('a3', '2 q'),
]
- normalized_encoded_request_params = sorted(
- [(quote(k), quote(v)) for k, v in unnormalized_request_parameters
- if k.lower() != "oauth realm"])
- client_secret = b"ECrDNoq1VYzzzzzzzzzyAK7TwZNtPnkqatqZZZZ"
- resource_owner_secret = b"just-a-string asdasd"
- control_base_string = (
- "POST&http%3A%2F%2Fexample.com%2Frequest&"
- "a2%3Dr%2520b%26"
- "a3%3D2%2520q%26"
- "a3%3Da%26"
- "b5%3D%253D%25253D%26"
- "c%2540%3D%26"
- "c2%3D%26"
- "oauth_consumer_key%3D9djdj82h48djs9d2%26"
- "oauth_nonce%3D7d8f3e4a%26"
- "oauth_signature_method%3DHMAC-SHA1%26"
- "oauth_timestamp%3D137131201%26"
- "oauth_token%3Dkkk9d7dh3k39sjv7"
- )
-
- def setUp(self):
- self.client = self.MockClient(
- client_secret = self.client_secret,
- resource_owner_secret = self.resource_owner_secret
- )
+
+ # The above parameters correspond to parameters from the three values below.
+ #
+ # These come from RFC 5849 section 3.4.1.3.1.
+
+ eg_uri_query = 'b5=%3D%253D&a3=a&c%40=&a2=r%20b'
+
+ eg_body = 'c2&a3=2+q'
+
+ eg_authorization_header =\
+ 'OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2",' \
+ ' oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1",' \
+ ' oauth_timestamp="137131201", oauth_nonce="7d8f3e4a",' \
+ ' oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"'
+
+ # ==== Signature base string calculating function tests ==========
def test_signature_base_string(self):
"""
- Example text to be turned into a base string::
-
- POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
- Host: example.com
- Content-Type: application/x-www-form-urlencoded
- Authorization: OAuth realm="Example",
- oauth_consumer_key="9djdj82h48djs9d2",
- oauth_token="kkk9d7dh3k39sjv7",
- oauth_signature_method="HMAC-SHA1",
- oauth_timestamp="137131201",
- oauth_nonce="7d8f3e4a",
- oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
- c2&a3=2+q
-
- Sample Base string generated and tested against::
- POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q
- %26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_
- key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m
- ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
- 9d7dh3k39sjv7
+ Test the ``signature_base_string`` function.
"""
- self.assertRaises(ValueError, base_string_uri, self.base_string_url)
- base_string_url = base_string_uri(self.base_string_url.decode('utf-8'))
- base_string_url = base_string_url.encode('utf-8')
- querystring = self.base_string_url.split(b'?', 1)[1]
- query_params = collect_parameters(querystring.decode('utf-8'),
- body=self.body)
- normalized_encoded_query_params = sorted(
- [(quote(k), quote(v)) for k, v in query_params])
- normalized_request_string = "&".join(sorted(
- ['='.join((k, v)) for k, v in (
- self.normalized_encoded_request_params +
- normalized_encoded_query_params)
- if k.lower() != 'oauth_signature']))
- self.assertRaises(ValueError, signature_base_string,
- self.http_method,
- base_string_url,
- normalized_request_string)
- self.assertRaises(ValueError, signature_base_string,
- self.http_method.decode('utf-8'),
- base_string_url,
- normalized_request_string)
-
- base_string = signature_base_string(
- self.http_method.decode('utf-8'),
- base_string_url.decode('utf-8'),
- normalized_request_string
- )
-
- self.assertEqual(self.control_base_string, base_string)
+ # Example from RFC 5849
- def test_base_string_uri(self):
- """
- Example text to be turned into a normalized base string uri::
+ self.assertEqual(
+ self.eg_signature_base_string,
+ signature_base_string(
+ self.eg_http_method,
+ self.eg_base_string_uri,
+ self.eg_normalized_parameters))
- GET /?q=1 HTTP/1.1
- Host: www.example.net:8080
+ # Test method is always uppercase in the signature base string
- Sample string generated::
+ for test_method in ['POST', 'Post', 'pOST', 'poST', 'posT', 'post']:
+ self.assertEqual(
+ self.eg_signature_base_string,
+ signature_base_string(
+ test_method,
+ self.eg_base_string_uri,
+ self.eg_normalized_parameters))
- https://www.example.net:8080/
+ def test_base_string_uri(self):
+ """
+ Test the ``base_string_uri`` function.
"""
- # test first example from RFC 5849 section 3.4.1.2.
+ # ----------------
+ # Examples from the OAuth 1.0a specification: RFC 5849.
+
+ # First example from RFC 5849 section 3.4.1.2.
+ #
+ # GET /r%20v/X?id=123 HTTP/1.1
+ # Host: EXAMPLE.COM:80
+ #
# Note: there is a space between "r" and "v"
- uri = 'http://EXAMPLE.COM:80/r v/X?id=123'
- self.assertEqual(base_string_uri(uri),
- 'http://example.com/r%20v/X')
-
- # test second example from RFC 5849 section 3.4.1.2.
- uri = 'https://www.example.net:8080/?q=1'
- self.assertEqual(base_string_uri(uri),
- 'https://www.example.net:8080/')
-
- # test for unicode failure
- uri = b"www.example.com:8080"
- self.assertRaises(ValueError, base_string_uri, uri)
-
- # test for missing scheme
- uri = "www.example.com:8080"
- self.assertRaises(ValueError, base_string_uri, uri)
-
- # test a URI with the default port
- uri = "http://www.example.com:80/"
- self.assertEqual(base_string_uri(uri),
- "http://www.example.com/")
-
- # test a URI missing a path
- uri = "http://www.example.com"
- self.assertEqual(base_string_uri(uri),
- "http://www.example.com/")
-
- # test a relative URI
- uri = "/a-host-relative-uri"
- host = "www.example.com"
- self.assertRaises(ValueError, base_string_uri, (uri, host))
-
- # test overriding the URI's netloc with a host argument
- uri = "http://www.example.com/a-path"
- host = "alternatehost.example.com"
- self.assertEqual(base_string_uri(uri, host),
- "http://alternatehost.example.com/a-path")
+
+ self.assertEqual(
+ 'http://example.com/r%20v/X',
+ base_string_uri('http://EXAMPLE.COM:80/r v/X?id=123'))
+
+ # Second example from RFC 5849 section 3.4.1.2.
+ #
+ # GET /?q=1 HTTP/1.1
+ # Host: www.example.net:8080
+
+ self.assertEqual(
+ 'https://www.example.net:8080/',
+ base_string_uri('https://www.example.net:8080/?q=1'))
+
+ # ----------------
+ # Scheme: will always be in lowercase
+
+ for uri in [
+ 'foobar://www.example.com',
+ 'FOOBAR://www.example.com',
+ 'Foobar://www.example.com',
+ 'FooBar://www.example.com',
+ 'fOObAR://www.example.com',
+ ]:
+ self.assertEqual('foobar://www.example.com/', base_string_uri(uri))
+
+ # ----------------
+ # Host: will always be in lowercase
+
+ for uri in [
+ 'http://www.example.com',
+ 'http://WWW.EXAMPLE.COM',
+ 'http://www.EXAMPLE.com',
+ 'http://wWW.eXAMPLE.cOM',
+ ]:
+ self.assertEqual('http://www.example.com/', base_string_uri(uri))
+
+ # base_string_uri has an optional host parameter that can be used to
+ # override the URI's netloc (or used as the host if there is no netloc)
+ # The "netloc" refers to the "hostname[:port]" part of the URI.
+
+ self.assertEqual(
+ 'http://actual.example.com/',
+ base_string_uri('http://IGNORE.example.com', 'ACTUAL.example.com'))
+
+ self.assertEqual(
+ 'http://override.example.com/path',
+ base_string_uri('http:///path', 'OVERRIDE.example.com'))
+
+ # ----------------
+ # Port: default ports always excluded; non-default ports always included
+
+ self.assertEqual(
+ "http://www.example.com/",
+ base_string_uri("http://www.example.com:80/")) # default port
+
+ self.assertEqual(
+ "https://www.example.com/",
+ base_string_uri("https://www.example.com:443/")) # default port
+
+ self.assertEqual(
+ "https://www.example.com:999/",
+ base_string_uri("https://www.example.com:999/")) # non-default port
+
+ self.assertEqual(
+ "http://www.example.com:443/",
+ base_string_uri("HTTP://www.example.com:443/")) # non-default port
+
+ self.assertEqual(
+ "https://www.example.com:80/",
+ base_string_uri("HTTPS://www.example.com:80/")) # non-default port
+
+ self.assertEqual(
+ "http://www.example.com/",
+ base_string_uri("http://www.example.com:/")) # colon but no number
+
+ # ----------------
+ # Paths
+
+ self.assertEqual(
+ 'http://www.example.com/',
+ base_string_uri('http://www.example.com')) # no slash
+
+ self.assertEqual(
+ 'http://www.example.com/',
+ base_string_uri('http://www.example.com/')) # with slash
+
+ self.assertEqual(
+ 'http://www.example.com:8080/',
+ base_string_uri('http://www.example.com:8080')) # no slash
+
+ self.assertEqual(
+ 'http://www.example.com:8080/',
+ base_string_uri('http://www.example.com:8080/')) # with slash
+
+ self.assertEqual(
+ 'http://www.example.com/foo/bar',
+ base_string_uri('http://www.example.com/foo/bar')) # no slash
+ self.assertEqual(
+ 'http://www.example.com/foo/bar/',
+ base_string_uri('http://www.example.com/foo/bar/')) # with slash
+
+ # ----------------
+ # Query parameters & fragment IDs do not appear in the base string URI
+
+ self.assertEqual(
+ 'https://www.example.com/path',
+ base_string_uri('https://www.example.com/path?foo=bar'))
+
+ self.assertEqual(
+ 'https://www.example.com/path',
+ base_string_uri('https://www.example.com/path#fragment'))
+
+ # ----------------
+ # Percent encoding
+ #
+ # RFC 5849 does not specify what characters are percent encoded, but in
+ # one of its examples it shows spaces being percent encoded.
+ # So it is assumed that spaces must be encoded, but we don't know what
+ # other characters are encoded or not.
+
+ self.assertEqual(
+ 'https://www.example.com/hello%20world',
+ base_string_uri('https://www.example.com/hello world'))
+
+ self.assertEqual(
+ 'https://www.hello%20world.com/',
+ base_string_uri('https://www.hello world.com/'))
+
+ # ----------------
+ # Errors detected
+
+ # base_string_uri expects a string
+ self.assertRaises(ValueError, base_string_uri, None)
+ self.assertRaises(ValueError, base_string_uri, 42)
+ self.assertRaises(ValueError, base_string_uri, b'http://example.com')
+
+ # Missing scheme is an error
+ self.assertRaises(ValueError, base_string_uri, '')
+ self.assertRaises(ValueError, base_string_uri, ' ') # single space
+ self.assertRaises(ValueError, base_string_uri, 'http')
+ self.assertRaises(ValueError, base_string_uri, 'example.com')
+
+ # Missing host is an error
+ self.assertRaises(ValueError, base_string_uri, 'http:')
+ self.assertRaises(ValueError, base_string_uri, 'http://')
+ self.assertRaises(ValueError, base_string_uri, 'http://:8080')
+
+ # Port is not a valid TCP/IP port number
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:0')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:-1')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:65536')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:3.14')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:BAD')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:NaN')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com: ')
+ self.assertRaises(ValueError, base_string_uri, 'http://eg.com:42:42')
def test_collect_parameters(self):
- """We check against parameters multiple times in case things change
- after more parameters are added.
"""
- self.assertEqual(collect_parameters(), [])
-
- # Check against uri_query
- parameters = collect_parameters(uri_query=self.uri_query)
- correct_parameters = [('b5', '=%3D'),
- ('a3', 'a'),
- ('c@', ''),
- ('a2', 'r b')]
- self.assertEqual(sorted(parameters), sorted(correct_parameters))
-
- headers = {'Authorization': self.authorization_header}
- # check against authorization header as well
- parameters = collect_parameters(
- uri_query=self.uri_query, headers=headers)
- parameters_with_realm = collect_parameters(
- uri_query=self.uri_query, headers=headers, with_realm=True)
- # Redo the checks against all the parameters. Duplicated code but
- # better safety
- correct_parameters += [
- ('oauth_nonce', '7d8f3e4a'),
- ('oauth_timestamp', '137131201'),
- ('oauth_consumer_key', '9djdj82h48djs9d2'),
- ('oauth_signature_method', 'HMAC-SHA1'),
- ('oauth_token', 'kkk9d7dh3k39sjv7')]
- correct_parameters_with_realm = (
- correct_parameters + [('realm', 'Example')])
- self.assertEqual(sorted(parameters), sorted(correct_parameters))
- self.assertEqual(sorted(parameters_with_realm),
- sorted(correct_parameters_with_realm))
-
- # Add in the body.
- # Redo again the checks against all the parameters. Duplicated code
- # but better safety
- parameters = collect_parameters(
- uri_query=self.uri_query, body=self.body, headers=headers)
- correct_parameters += [
- ('c2', ''),
- ('a3', '2 q')
- ]
- self.assertEqual(sorted(parameters), sorted(correct_parameters))
+ Test the ``collect_parameters`` function.
+ """
+
+ # ----------------
+ # Examples from the OAuth 1.0a specification: RFC 5849.
+
+ params = collect_parameters(
+ self.eg_uri_query,
+ self.eg_body,
+ {'Authorization': self.eg_authorization_header})
+
+ # Check params contains the same pairs as control_params, ignoring order
+ self.assertEqual(sorted(self.eg_params), sorted(params))
+
+ # ----------------
+ # Examples with no parameters
+
+ self.assertEqual([], collect_parameters('', '', {}))
+
+ self.assertEqual([], collect_parameters(None, None, None))
+
+ self.assertEqual([], collect_parameters())
+
+ self.assertEqual([], collect_parameters(headers={'foo': 'bar'}))
+
+ # ----------------
+ # Test effect of exclude_oauth_signature"
+
+ no_sig = collect_parameters(
+ headers={'authorization': self.eg_authorization_header})
+ with_sig = collect_parameters(
+ headers={'authorization': self.eg_authorization_header},
+ exclude_oauth_signature=False)
+
+ self.assertEqual(sorted(no_sig + [('oauth_signature',
+ 'djosJKDKJSD8743243/jdk33klY=')]),
+ sorted(with_sig))
+
+ # ----------------
+ # Test effect of "with_realm" as well as header name case insensitivity
+
+ no_realm = collect_parameters(
+ headers={'authorization': self.eg_authorization_header},
+ with_realm=False)
+ with_realm = collect_parameters(
+ headers={'AUTHORIZATION': self.eg_authorization_header},
+ with_realm=True)
+
+ self.assertEqual(sorted(no_realm + [('realm', 'Example')]),
+ sorted(with_realm))
def test_normalize_parameters(self):
- """ We copy some of the variables from the test method above."""
-
- headers = {'Authorization': self.authorization_header}
- parameters = collect_parameters(
- uri_query=self.uri_query, body=self.body, headers=headers)
- normalized = normalize_parameters(parameters)
-
- # Unicode everywhere and always
- self.assertIsInstance(normalized, str)
-
- # Lets see if things are in order
- # check to see that querystring keys come in alphanumeric order:
- querystring_keys = ['a2', 'a3', 'b5', 'oauth_consumer_key',
- 'oauth_nonce', 'oauth_signature_method',
- 'oauth_timestamp', 'oauth_token']
- index = -1 # start at -1 because the 'a2' key starts at index 0
- for key in querystring_keys:
- self.assertGreater(normalized.index(key), index)
- index = normalized.index(key)
-
- # Control signature created using openssl:
- # echo -n $(cat <message>) | openssl dgst -binary -hmac <key> | base64
- control_signature = "mwd09YMxVd2XJ1gudNaBuAuKKuY="
- control_signature_s = "wsdNmjGB7lvis0UJuPAmjvX/PXw="
-
- def test_sign_hmac_sha1(self):
- """Verifying HMAC-SHA1 signature against one created by OpenSSL."""
-
- self.assertRaises(ValueError, sign_hmac_sha1, self.control_base_string,
- self.client_secret, self.resource_owner_secret)
-
- sign = sign_hmac_sha1(self.control_base_string,
- self.client_secret.decode('utf-8'),
- b'')
- self.assertEqual(len(sign), 28)
- self.assertEqual(sign, self.control_signature)
-
- def test_sign_hmac_sha1_with_secret(self):
- """Verifying HMAC-SHA1 signature against one created by OpenSSL."""
-
- self.assertRaises(ValueError, sign_hmac_sha1, self.control_base_string,
- self.client_secret, self.resource_owner_secret)
-
- sign = sign_hmac_sha1(self.control_base_string,
- self.client_secret.decode('utf-8'),
- self.resource_owner_secret.decode('utf-8'))
- self.assertEqual(len(sign), 28)
- self.assertEqual(sign, self.control_signature_s)
+ """
+ Test the ``normalize_parameters`` function.
+ """
+
+ # headers = {'Authorization': self.authorization_header}
+ # parameters = collect_parameters(
+ # uri_query=self.uri_query, body=self.body, headers=headers)
+ # normalized = normalize_parameters(parameters)
+ #
+ # # Unicode everywhere and always
+ # self.assertIsInstance(normalized, str)
+ #
+ # # Lets see if things are in order
+ # # check to see that querystring keys come in alphanumeric order:
+ # querystring_keys = ['a2', 'a3', 'b5', 'oauth_consumer_key',
+ # 'oauth_nonce', 'oauth_signature_method',
+ # 'oauth_timestamp', 'oauth_token']
+ # index = -1 # start at -1 because the 'a2' key starts at index 0
+ # for key in querystring_keys:
+ # self.assertGreater(normalized.index(key), index)
+ # index = normalized.index(key)
+
+ # ----------------
+ # Example from the OAuth 1.0a specification: RFC 5849.
+ # Params from end of section 3.4.1.3.1. and the expected
+ # normalized parameters from the end of section 3.4.1.3.2.
+
+ self.assertEqual(self.eg_normalized_parameters,
+ normalize_parameters(self.eg_params))
+
+ # ==== HMAC-based signature method tests =========================
+
+ hmac_client = MockClient(
+ client_secret='ECrDNoq1VYzzzzzzzzzyAK7TwZNtPnkqatqZZZZ',
+ resource_owner_secret='just-a-string asdasd')
+
+ # The following expected signatures were calculated by putting the value of
+ # the eg_signature_base_string in a file ("base-str.txt") and running:
+ #
+ # echo -n `cat base-str.txt` | openssl dgst -hmac KEY -sha1 -binary| base64
+ #
+ # Where the KEY is the concatenation of the client_secret, an ampersand and
+ # the resource_owner_secret. But those values need to be encoded properly,
+ # so the spaces in the resource_owner_secret must be represented as '%20'.
+ #
+ # Note: the "echo -n" is needed to remove the last newline character, which
+ # most text editors will add.
+
+ expected_signature_hmac_sha1 = \
+ 'wsdNmjGB7lvis0UJuPAmjvX/PXw='
+
+ expected_signature_hmac_sha256 = \
+ 'wdfdHUKXHbOnOGZP8WFAWMSAmWzN3EVBWWgXGlC/Eo4='
+
+ expected_signature_hmac_sha512 = \
+ 'u/vlyZFDxOWOZ9UUXwRBJHvq8/T4jCA74ocRmn2ECnjUBTAeJiZIRU8hDTjS88Tz' \
+ '1fGONffMpdZxUkUTW3k1kg=='
def test_sign_hmac_sha1_with_client(self):
- self.assertRaises(ValueError,
- sign_hmac_sha1_with_client,
- self.control_base_string,
- self.client)
-
- self.client.decode()
- sign = sign_hmac_sha1_with_client(
- self.control_base_string, self.client)
-
- self.assertEqual(len(sign), 28)
- self.assertEqual(sign, self.control_signature_s)
-
-
- control_base_string_rsa_sha1 = (
- b"POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q"
- b"%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_"
- b"key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m"
- b"ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk"
- b"9d7dh3k39sjv7"
- )
-
- # Generated using: $ openssl genrsa -out <key>.pem 1024
- # PEM encoding requires the key to be concatenated with
- # linebreaks.
- rsa_private_key = b"""-----BEGIN RSA PRIVATE KEY-----
+ """
+ Test sign and verify with HMAC-SHA1.
+ """
+ self.assertEqual(
+ self.expected_signature_hmac_sha1,
+ sign_hmac_sha1_with_client(self.eg_signature_base_string,
+ self.hmac_client))
+ self.assertTrue(verify_hmac_sha1(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_hmac_sha1),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ def test_sign_hmac_sha256_with_client(self):
+ """
+ Test sign and verify with HMAC-SHA256.
+ """
+ self.assertEqual(
+ self.expected_signature_hmac_sha256,
+ sign_hmac_sha256_with_client(self.eg_signature_base_string,
+ self.hmac_client))
+ self.assertTrue(verify_hmac_sha256(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_hmac_sha256),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ def test_sign_hmac_sha512_with_client(self):
+ """
+ Test sign and verify with HMAC-SHA512.
+ """
+ self.assertEqual(
+ self.expected_signature_hmac_sha512,
+ sign_hmac_sha512_with_client(self.eg_signature_base_string,
+ self.hmac_client))
+ self.assertTrue(verify_hmac_sha512(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_hmac_sha512),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ def test_hmac_false_positives(self):
+ """
+ Test verify_hmac-* functions will correctly detect invalid signatures.
+ """
+
+ _ros = self.hmac_client.resource_owner_secret
+
+ for functions in [
+ (sign_hmac_sha1_with_client, verify_hmac_sha1),
+ (sign_hmac_sha256_with_client, verify_hmac_sha256),
+ (sign_hmac_sha512_with_client, verify_hmac_sha512),
+ ]:
+ signing_function = functions[0]
+ verify_function = functions[1]
+
+ good_signature = \
+ signing_function(
+ self.eg_signature_base_string,
+ self.hmac_client)
+
+ bad_signature_on_different_value = \
+ signing_function(
+ 'not the signature base string',
+ self.hmac_client)
+
+ bad_signature_produced_by_different_client_secret = \
+ signing_function(
+ self.eg_signature_base_string,
+ MockClient(client_secret='wrong-secret',
+ resource_owner_secret=_ros))
+ bad_signature_produced_by_different_resource_owner_secret = \
+ signing_function(
+ self.eg_signature_base_string,
+ MockClient(client_secret=self.hmac_client.client_secret,
+ resource_owner_secret='wrong-secret'))
+
+ bad_signature_produced_with_no_resource_owner_secret = \
+ signing_function(
+ self.eg_signature_base_string,
+ MockClient(client_secret=self.hmac_client.client_secret))
+ bad_signature_produced_with_no_client_secret = \
+ signing_function(
+ self.eg_signature_base_string,
+ MockClient(resource_owner_secret=_ros))
+
+ self.assertTrue(verify_function(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ good_signature),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ for bad_signature in [
+ '',
+ 'ZG9uJ3QgdHJ1c3QgbWUK', # random base64 encoded value
+ 'altérer', # value with a non-ASCII character in it
+ bad_signature_on_different_value,
+ bad_signature_produced_by_different_client_secret,
+ bad_signature_produced_by_different_resource_owner_secret,
+ bad_signature_produced_with_no_resource_owner_secret,
+ bad_signature_produced_with_no_client_secret,
+ ]:
+ self.assertFalse(verify_function(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ bad_signature),
+ self.hmac_client.client_secret,
+ self.hmac_client.resource_owner_secret))
+
+ # ==== RSA-based signature methods tests =========================
+
+ rsa_private_client = MockClient(rsa_key='''
+-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDk1/bxyS8Q8jiheHeYYp/4rEKJopeQRRKKpZI4s5i+UPwVpupG
AlwXWfzXwSMaKPAoKJNdu7tqKRniqst5uoHXw98gj0x7zamu0Ck1LtQ4c7pFMVah
5IYGhBi2E9ycNS329W27nJPWNCbESTu7snVlG8V8mfvGGg3xNjTMO7IdrwIDAQAB
@@ -310,71 +597,291 @@ kmaMg2PNrjUR51F0zOEFycaaqXbGcFwe1/xx9zLmHzMDXd4bsnwt9kk+fe0hQzVS
JzatanQit3+feev1PN3QewJAWv4RZeavEUhKv+kLe95Yd0su7lTLVduVgh4v5yLT
Ga6FHdjGPcfajt+nrpB1n8UQBEH9ZxniokR/IPvdMlxqXA==
-----END RSA PRIVATE KEY-----
-"""
- @property
- def control_signature_rsa_sha1(self):
- # Base string saved in "<message>". Signature obtained using:
- # $ echo -n $(cat <msg>) | openssl dgst -sha1 -sign <key>.pem | base64
- # where echo -n suppresses the last linebreak.
- return (
- "mFY2KOEnlYWsTvUA+5kxuBIcvBYXu+ljw9ttVJQxKduMueGSVPCB1tK1PlqVLK738"
- "HK0t19ecBJfb6rMxUwrriw+MlBO+jpojkZIWccw1J4cAb4qu4M81DbpUAq4j/1w/Q"
- "yTR4TWCODlEfN7Zfgy8+pf+TjiXfIwRC1jEWbuL1E="
-
- )
-
- def test_sign_rsa_sha1(self):
- """Verify RSA-SHA1 signature against one created by OpenSSL."""
- base_string = self.control_base_string_rsa_sha1
+''')
+
+ rsa_public_client = MockClient(rsa_key='''
+-----BEGIN RSA PUBLIC KEY-----
+MIGJAoGBAOTX9vHJLxDyOKF4d5hin/isQomil5BFEoqlkjizmL5Q/BWm6kYCXBdZ
+/NfBIxoo8Cgok127u2opGeKqy3m6gdfD3yCPTHvNqa7QKTUu1DhzukUxVqHkhgaE
+GLYT3Jw1Lfb1bbuck9Y0JsRJO7uydWUbxXyZ+8YaDfE2NMw7sh2vAgMBAAE=
+-----END RSA PUBLIC KEY-----
+''')
+
+ # The above private key was generated using:
+ # $ openssl genrsa -out example.pvt 1024
+ # $ chmod 600 example.pvt
+ # Public key was extract from it using:
+ # $ ssh-keygen -e -m pem -f example.pvt
+ # PEM encoding requires the key to be concatenated with linebreaks.
+
+ # The following expected signatures were calculated by putting the private
+ # key in a file (test.pvt) and the value of sig_base_str_rsa in another file
+ # ("base-str.txt") and running:
+ #
+ # echo -n `cat base-str.txt` | openssl dgst -sha1 -sign test.pvt| base64
+ #
+ # Note: the "echo -n" is needed to remove the last newline character, which
+ # most text editors will add.
+
+ expected_signature_rsa_sha1 = \
+ 'mFY2KOEnlYWsTvUA+5kxuBIcvBYXu+ljw9ttVJQxKduMueGSVPCB1tK1PlqVLK738' \
+ 'HK0t19ecBJfb6rMxUwrriw+MlBO+jpojkZIWccw1J4cAb4qu4M81DbpUAq4j/1w/Q' \
+ 'yTR4TWCODlEfN7Zfgy8+pf+TjiXfIwRC1jEWbuL1E='
+
+ expected_signature_rsa_sha256 = \
+ 'jqKl6m0WS69tiVJV8ZQ6aQEfJqISoZkiPBXRv6Al2+iFSaDpfeXjYm+Hbx6m1azR' \
+ 'drZ/35PM3cvuid3LwW/siAkzb0xQcGnTyAPH8YcGWzmnKGY7LsB7fkqThchNxvRK' \
+ '/N7s9M1WMnfZZ+1dQbbwtTs1TG1+iexUcV7r3M7Heec='
+
+ expected_signature_rsa_sha512 = \
+ 'jL1CnjlsNd25qoZVHZ2oJft47IRYTjpF5CvCUjL3LY0NTnbEeVhE4amWXUFBe9GL' \
+ 'DWdUh/79ZWNOrCirBFIP26cHLApjYdt4ZG7EVK0/GubS2v8wT1QPRsog8zyiMZkm' \
+ 'g4JXdWCGXG8YRvRJTg+QKhXuXwS6TcMNakrgzgFIVhA='
- private_key = self.rsa_private_key
+ def test_sign_rsa_sha1_with_client(self):
+ """
+ Test sign and verify with RSA-SHA1.
+ """
+ self.assertEqual(
+ self.expected_signature_rsa_sha1,
+ sign_rsa_sha1_with_client(self.eg_signature_base_string,
+ self.rsa_private_client))
+ self.assertTrue(verify_rsa_sha1(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_rsa_sha1),
+ self.rsa_public_client.rsa_key))
+
+ def test_sign_rsa_sha256_with_client(self):
+ """
+ Test sign and verify with RSA-SHA256.
+ """
+ self.assertEqual(
+ self.expected_signature_rsa_sha256,
+ sign_rsa_sha256_with_client(self.eg_signature_base_string,
+ self.rsa_private_client))
+ self.assertTrue(verify_rsa_sha256(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_rsa_sha256),
+ self.rsa_public_client.rsa_key))
+
+ def test_sign_rsa_sha512_with_client(self):
+ """
+ Test sign and verify with RSA-SHA512.
+ """
+ self.assertEqual(
+ self.expected_signature_rsa_sha512,
+ sign_rsa_sha512_with_client(self.eg_signature_base_string,
+ self.rsa_private_client))
+ self.assertTrue(verify_rsa_sha512(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_rsa_sha512),
+ self.rsa_public_client.rsa_key))
+
+ def test_rsa_false_positives(self):
+ """
+ Test verify_rsa-* functions will correctly detect invalid signatures.
+ """
- control_signature = self.control_signature_rsa_sha1
+ another_client = MockClient(rsa_key='''
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDZcD/1OZNJJ6Y3QZM16Z+O7fkD9kTIQuT2BfpAOUvDfxzYhVC9
+TNmSDHCQhr+ClutyolBk5jTE1/FXFUuHoPsTrkI7KQFXPP834D4gnSY9jrAiUJHe
+DVF6wXNuS7H4Ueh16YPjUxgLLRh/nn/JSEj98gsw+7DP01OWMfWS99S7eQIDAQAB
+AoGBALsQZRXVyK7BG7CiC8HwEcNnXDpaXmZjlpNKJTenk1THQMvONd4GBZAuf5D3
+PD9fE4R1u/ByVKecmBaxTV+L0TRQfD8K/nbQe0SKRQIkLI2ymLJKC/eyw5iTKT0E
++BS6wYpVd+mfcqgvpHOYpUmz9X8k/eOa7uslFmvt+sDb5ZcBAkEA+++SRqqUxFEG
+s/ZWAKw9p5YgkeVUOYVUwyAeZ97heySrjVzg1nZ6v6kv7iOPi9KOEpaIGPW7x1K/
+uQuSt4YEqQJBANzyNqZTTPpv7b/R8ABFy0YMwPVNt3b1GOU1Xxl6iuhH2WcHuueo
+UB13JHoZCMZ7hsEqieEz6uteUjdRzRPKclECQFNhVK4iop3emzNQYeJTHwyp+RmQ
+JrHq2MTDioyiDUouNsDQbnFMQQ/RtNVB265Q/0hTnbN1ELLFRkK9+87VghECQQC9
+hacLFPk6+TffCp3sHfI3rEj4Iin1iFhKhHWGzW7JwJfjoOXaQK44GDLZ6Q918g+t
+MmgDHR2tt8KeYTSgfU+BAkBcaVF91EQ7VXhvyABNYjeYP7lU7orOgdWMa/zbLXSU
+4vLsK1WOmwPY9zsXpPkilqszqcru4gzlG462cSbEdAW9
+-----END RSA PRIVATE KEY-----
+''')
+
+ for functions in [
+ (sign_rsa_sha1_with_client, verify_rsa_sha1),
+ (sign_rsa_sha256_with_client, verify_rsa_sha256),
+ (sign_rsa_sha512_with_client, verify_rsa_sha512),
+ ]:
+ signing_function = functions[0]
+ verify_function = functions[1]
+
+ good_signature = \
+ signing_function(self.eg_signature_base_string,
+ self.rsa_private_client)
+
+ bad_signature_on_different_value = \
+ signing_function('wrong value signed', self.rsa_private_client)
+
+ bad_signature_produced_by_different_private_key = \
+ signing_function(self.eg_signature_base_string, another_client)
+
+ self.assertTrue(verify_function(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ good_signature),
+ self.rsa_public_client.rsa_key))
+
+ for bad_signature in [
+ '',
+ 'ZG9uJ3QgdHJ1c3QgbWUK', # random base64 encoded value
+ 'altérer', # value with a non-ASCII character in it
+ bad_signature_on_different_value,
+ bad_signature_produced_by_different_private_key,
+ ]:
+ self.assertFalse(verify_function(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ bad_signature),
+ self.rsa_public_client.rsa_key))
+
+ def test_rsa_bad_keys(self):
+ """
+ Testing RSA sign and verify with bad key values produces errors.
- sign = sign_rsa_sha1(base_string, private_key)
- self.assertEqual(sign, control_signature)
- sign = sign_rsa_sha1(base_string.decode('utf-8'), private_key)
- self.assertEqual(sign, control_signature)
+ This test is useful for coverage tests, since it runs the code branches
+ that deal with error situations.
+ """
+ # Signing needs a private key
- def test_sign_rsa_sha1_with_client(self):
- base_string = self.control_base_string_rsa_sha1
+ for bad_value in [None, '', 'foobar']:
+ self.assertRaises(ValueError,
+ sign_rsa_sha1_with_client,
+ self.eg_signature_base_string,
+ MockClient(rsa_key=bad_value))
- self.client.rsa_key = self.rsa_private_key
+ self.assertRaises(AttributeError,
+ sign_rsa_sha1_with_client,
+ self.eg_signature_base_string,
+ self.rsa_public_client) # public key doesn't sign
- control_signature = self.control_signature_rsa_sha1
+ # Verify needs a public key
- sign = sign_rsa_sha1_with_client(base_string, self.client)
+ for bad_value in [None, '', 'foobar', self.rsa_private_client.rsa_key]:
+ self.assertRaises(TypeError,
+ verify_rsa_sha1,
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ self.expected_signature_rsa_sha1),
+ MockClient(rsa_key=bad_value))
- self.assertEqual(sign, control_signature)
+ # For completeness, this text could repeat the above for RSA-SHA256 and
+ # RSA-SHA512 signing and verification functions.
- self.client.decode() ## Decode `rsa_private_key` from UTF-8
+ def test_rsa_jwt_algorithm_cache(self):
+ # Tests cache of RSAAlgorithm objects is implemented correctly.
- sign = sign_rsa_sha1_with_client(base_string, self.client)
+ # This is difficult to test, since the cache is internal.
+ #
+ # Running this test with coverage will show the cache-hit branch of code
+ # being executed by two signing operations with the same hash algorithm.
- self.assertEqual(sign, control_signature)
+ self.test_sign_rsa_sha1_with_client() # creates cache entry
+ self.test_sign_rsa_sha1_with_client() # reuses cache entry
+ # Some possible bugs will be detected if multiple signing operations
+ # with different hash algorithms produce the wrong results (e.g. if the
+ # cache incorrectly returned the previously used algorithm, instead
+ # of the one that is needed).
- control_signature_plaintext = (
- "ECrDNoq1VYzzzzzzzzzyAK7TwZNtPnkqatqZZZZ&"
- "just-a-string%20%20%20%20asdasd")
+ self.test_sign_rsa_sha256_with_client()
+ self.test_sign_rsa_sha256_with_client()
+ self.test_sign_rsa_sha1_with_client()
+ self.test_sign_rsa_sha256_with_client()
+ self.test_sign_rsa_sha512_with_client()
- def test_sign_plaintext(self):
- """ """
+ # ==== PLAINTEXT signature method tests ==========================
- self.assertRaises(ValueError, sign_plaintext, self.client_secret,
- self.resource_owner_secret)
- sign = sign_plaintext(self.client_secret.decode('utf-8'),
- self.resource_owner_secret.decode('utf-8'))
- self.assertEqual(sign, self.control_signature_plaintext)
+ plaintext_client = hmac_client # for convenience, use the same HMAC secrets
+ expected_signature_plaintext = (
+ 'ECrDNoq1VYzzzzzzzzzyAK7TwZNtPnkqatqZZZZ'
+ '&'
+ 'just-a-string%20%20%20%20asdasd')
def test_sign_plaintext_with_client(self):
- self.assertRaises(ValueError, sign_plaintext_with_client,
- None, self.client)
-
- self.client.decode()
-
- sign = sign_plaintext_with_client(None, self.client)
+ # With PLAINTEXT, the "signature" is always the same: regardless of the
+ # contents of the request. It is the concatenation of the encoded
+ # client_secret, an ampersand, and the encoded resource_owner_secret.
+ #
+ # That is why the spaces in the resource owner secret are "%20".
+
+ self.assertEqual(self.expected_signature_plaintext,
+ sign_plaintext_with_client(None, # request is ignored
+ self.plaintext_client))
+ self.assertTrue(verify_plaintext(
+ MockRequest('PUT',
+ 'http://example.com/some-other-path',
+ [('description', 'request is ignored in PLAINTEXT')],
+ self.expected_signature_plaintext),
+ self.plaintext_client.client_secret,
+ self.plaintext_client.resource_owner_secret))
+
+ def test_plaintext_false_positives(self):
+ """
+ Test verify_plaintext function will correctly detect invalid signatures.
+ """
- self.assertEqual(sign, self.control_signature_plaintext)
+ _ros = self.plaintext_client.resource_owner_secret
+
+ good_signature = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ self.plaintext_client)
+
+ bad_signature_produced_by_different_client_secret = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ MockClient(client_secret='wrong-secret',
+ resource_owner_secret=_ros))
+ bad_signature_produced_by_different_resource_owner_secret = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ MockClient(client_secret=self.plaintext_client.client_secret,
+ resource_owner_secret='wrong-secret'))
+
+ bad_signature_produced_with_no_resource_owner_secret = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ MockClient(client_secret=self.plaintext_client.client_secret))
+ bad_signature_produced_with_no_client_secret = \
+ sign_plaintext_with_client(
+ self.eg_signature_base_string,
+ MockClient(resource_owner_secret=_ros))
+
+ self.assertTrue(verify_plaintext(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ good_signature),
+ self.plaintext_client.client_secret,
+ self.plaintext_client.resource_owner_secret))
+
+ for bad_signature in [
+ '',
+ 'ZG9uJ3QgdHJ1c3QgbWUK', # random base64 encoded value
+ 'altérer', # value with a non-ASCII character in it
+ bad_signature_produced_by_different_client_secret,
+ bad_signature_produced_by_different_resource_owner_secret,
+ bad_signature_produced_with_no_resource_owner_secret,
+ bad_signature_produced_with_no_client_secret,
+ ]:
+ self.assertFalse(verify_plaintext(
+ MockRequest('POST',
+ 'http://example.com/request',
+ self.eg_params,
+ bad_signature),
+ self.plaintext_client.client_secret,
+ self.plaintext_client.resource_owner_secret))