summaryrefslogtreecommitdiff
path: root/oauthlib/oauth1/rfc5849/signature.py
diff options
context:
space:
mode:
Diffstat (limited to 'oauthlib/oauth1/rfc5849/signature.py')
-rw-r--r--oauthlib/oauth1/rfc5849/signature.py874
1 files changed, 491 insertions, 383 deletions
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):