about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/oauthlib/oauth1/rfc5849/signature.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/oauthlib/oauth1/rfc5849/signature.py')
-rw-r--r--.venv/lib/python3.12/site-packages/oauthlib/oauth1/rfc5849/signature.py852
1 files changed, 852 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/oauthlib/oauth1/rfc5849/signature.py b/.venv/lib/python3.12/site-packages/oauthlib/oauth1/rfc5849/signature.py
new file mode 100644
index 00000000..9cb1a517
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/oauthlib/oauth1/rfc5849/signature.py
@@ -0,0 +1,852 @@
+"""
+This module is an implementation of `section 3.4`_ of RFC 5849.
+
+**Usage**
+
+Steps for signing a request:
+
+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 ipaddress
+import logging
+import urllib.parse as urlparse
+import warnings
+
+from oauthlib.common import extract_params, safe_string_equals, urldecode
+
+from . import utils
+
+log = logging.getLogger(__name__)
+
+
+# ==== Common functions ==========================================
+
+def signature_base_string(
+        http_method: str,
+        base_str_uri: str,
+        normalized_encoded_request_parameters: str) -> str:
+    """
+    Construct the signature base string.
+
+    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.
+
+    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
+    """
+
+    # The signature base string is constructed by concatenating together,
+    # in order, the following HTTP request elements:
+
+    # 1.  The HTTP request method in uppercase.  For example: "HEAD",
+    #     "GET", "POST", etc.  If the request uses a custom HTTP method, it
+    #     MUST be encoded (`Section 3.6`_).
+    #
+    # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+    base_string = utils.escape(http_method.upper())
+
+    # 2.  An "&" character (ASCII code 38).
+    base_string += '&'
+
+    # 3.  The base string URI from `Section 3.4.1.2`_, after being encoded
+    #     (`Section 3.6`_).
+    #
+    # .. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
+    # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+    base_string += utils.escape(base_str_uri)
+
+    # 4.  An "&" character (ASCII code 38).
+    base_string += '&'
+
+    # 5.  The request parameters as normalized in `Section 3.4.1.3.2`_, after
+    #     being encoded (`Section 3.6`).
+    #
+    # .. _`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: str, host: str = None) -> str:
+    """
+    Calculates the _base string URI_.
+
+    The *base string URI* is one of the components that make up the
+     *signature base string*.
+
+    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).
+
+    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
+
+    :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 string.')
+
+    # FIXME: urlparse does not support unicode
+    output = urlparse.urlparse(uri)
+    scheme = output.scheme
+    hostname = output.hostname
+    port = output.port
+    path = output.path
+    params = output.params
+
+    # The scheme, authority, and path of the request resource URI `RFC3986`
+    # are included by constructing an "http" or "https" URI representing
+    # the request resource (without the query or fragment) as follows:
+    #
+    # .. _`RFC3986`: https://tools.ietf.org/html/rfc3986
+
+    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 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()
+    # Note: if ``host`` is used, it will be converted to lowercase below
+    if hostname is not None:
+        hostname = hostname.lower()
+
+    # 2.  The host and port values MUST match the content of the HTTP
+    #     request "Host" header field.
+    if host is not None:
+        # NOTE: override value in uri with provided host
+        # Host argument is equal to netloc. It means it's missing scheme.
+        # Add it back, before parsing.
+
+        host = host.lower()
+        host = f"{scheme}://{host}"
+        output = urlparse.urlparse(host)
+        hostname = output.hostname
+        port = output.port
+
+    # 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,
+    #     the port MUST be excluded when making an HTTP request `RFC2616`_
+    #     to port 80 or when making an HTTPS request `RFC2818`_ to port 443.
+    #     All other non-default port numbers MUST be included.
+    #
+    # .. _`RFC2616`: https://tools.ietf.org/html/rfc2616
+    # .. _`RFC2818`: https://tools.ietf.org/html/rfc2818
+
+    if hostname is None:
+        raise ValueError('missing host')
+
+    # NOTE: Try guessing if we're dealing with IP or hostname
+    try:
+        hostname = ipaddress.ip_address(hostname)
+    except ValueError:
+        pass
+
+    if isinstance(hostname, ipaddress.IPv6Address):
+        hostname = f"[{hostname}]"
+    elif isinstance(hostname, ipaddress.IPv4Address):
+        hostname = f"{hostname}"
+
+    if port is not None and not (0 < port <= 65535):
+        raise ValueError('port out of range')  # 16-bit unsigned ints
+    if (scheme, port) in (('http', 80), ('https', 443)):
+        netloc = hostname  # default port for scheme: exclude port num
+    elif port:
+        netloc = f"{hostname}:{port}"  # use hostname:port
+    else:
+        netloc = hostname
+
+    v = urlparse.urlunparse((scheme, netloc, path, params, '', ''))
+
+    # RFC 5849 does not specify which characters are encoded in the
+    # "base string URI", nor how they are encoded - which is very bad, since
+    # the signatures won't match if there are any differences. Fortunately,
+    # most URIs only use characters that are clearly not encoded (e.g. digits
+    # and A-Z, a-z), so have avoided any differences between implementations.
+    #
+    # The example from its section 3.4.1.2 illustrates that spaces in
+    # the path are percent encoded. But it provides no guidance as to what other
+    # characters (if any) must be encoded (nor how); nor if characters in the
+    # other components are to be encoded or not.
+    #
+    # This implementation **assumes** that **only** the space is percent-encoded
+    # and it is done to the entire value (not just to spaces in the path).
+    #
+    # This code may need to be changed if it is discovered that other characters
+    # are expected to be encoded.
+    #
+    # Note: the "base string URI" returned by this function will be encoded
+    # again before being concatenated into the "signature base string". So any
+    # spaces in the URI will actually appear in the "signature base string"
+    # as "%2520" (the "%20" further encoded according to section 3.6).
+
+    return v.replace(' ', '%20')
+
+
+def collect_parameters(uri_query='', body=None, headers=None,
+                       exclude_oauth_signature=True, with_realm=False):
+    """
+    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
+    form encoded query string.
+
+    Headers must be supplied as a dict.
+
+    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 = []
+
+    # The parameters from the following sources are collected into a single
+    # list of name/value pairs:
+
+    # *  The query component of the HTTP request URI as defined by
+    #    `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
+    #    `W3C-HTML-4.0`_, Section 17.13.4.
+    #
+    # .. _`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))
+
+    # *  The OAuth HTTP "Authorization" header field (`Section 3.5.1`_) if
+    #    present.  The header's content is parsed into a list of name/value
+    #    pairs excluding the "realm" parameter if present.  The parameter
+    #    values are decoded as defined by `Section 3.5.1`_.
+    #
+    # .. _`Section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1
+    if headers:
+        headers_lower = {k.lower(): v for k, v in headers.items()}
+        authorization_header = headers_lower.get('authorization')
+        if authorization_header is not None:
+            params.extend([i for i in utils.parse_authorization_header(
+                authorization_header) if with_realm or i[0] != 'realm'])
+
+    # *  The HTTP request entity-body, but only if all of the following
+    #    conditions are met:
+    #     *  The entity-body is single-part.
+    #
+    #     *  The entity-body follows the encoding requirements of the
+    #        "application/x-www-form-urlencoded" content-type as defined by
+    #        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-HTML-4.0`: https://www.w3.org/TR/1998/REC-html40-19980424/
+
+    # TODO: enforce header param inclusion conditions
+    bodyparams = extract_params(body) or []
+    params.extend(bodyparams)
+
+    # ensure all oauth params are unescaped
+    unescaped_params = []
+    for k, v in params:
+        if k.startswith('oauth_'):
+            v = utils.unescape(v)
+        unescaped_params.append((k, v))
+
+    # The "oauth_signature" parameter MUST be excluded from the signature
+    # base string if present.
+    if exclude_oauth_signature:
+        unescaped_params = list(filter(lambda i: i[0] != 'oauth_signature',
+                                       unescaped_params))
+
+    return unescaped_params
+
+
+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
+    # single string as follows:
+    #
+    # .. _`Section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3
+
+    # 1.  First, the name and value of each parameter are encoded
+    #     (`Section 3.6`_).
+    #
+    # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+    key_values = [(utils.escape(k), utils.escape(v)) for k, v in params]
+
+    # 2.  The parameters are sorted by name, using ascending byte value
+    #     ordering.  If two or more parameters share the same name, they
+    #     are sorted by their value.
+    key_values.sort()
+
+    # 3.  The name of each parameter is concatenated to its corresponding
+    #     value using an "=" character (ASCII code 61) as a separator, even
+    #     if the value is empty.
+    parameter_parts = ['{}={}'.format(k, v) for k, v in key_values]
+
+    # 4.  The sorted name/value pairs are concatenated together into a
+    #     single string by using an "&" character (ASCII code 38) as
+    #     separator.
+    return '&'.join(parameter_parts)
+
+
+# ==== 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**
+
+    The "HMAC-SHA256" signature method uses the HMAC-SHA256 signature
+    algorithm as defined in `RFC4634`_::
+
+        digest = HMAC-SHA256 (key, text)
+
+    Per `section 3.4.2`_ of the spec.
+
+    .. _`RFC4634`: https://tools.ietf.org/html/rfc4634
+    .. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2
+    """
+
+    # 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 = sig_base_str
+
+    # 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 '')
+
+    # 2.  An "&" character (ASCII code 38), which MUST be included
+    #     even when either secret is empty.
+    key += '&'
+
+    # 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 '')
+
+    # 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, 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, Sec 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
+    return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
+
+
+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):
+    """
+    Deprecated function for calculating a HMAC-SHA256 signature.
+
+    This function has been replaced by invoking ``sign_hmac`` with "SHA-256"
+    as the hash algorithm name.
+
+    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)
+
+    # 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-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.
+
+    This method maintains the ``_jwt_rsa`` cache.
+
+    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
+
+        # 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])
+
+        _jwt_rsa[hash_algorithm_name] = v  # populate cache
+
+        return v
+
+
+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)
+
+
+def _sign_rsa(hash_algorithm_name: str,
+              sig_base_str: str,
+              rsa_private_key: str):
+    """
+    Calculate the signature for an RSA-based signature method.
+
+    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.
+
+    Signing for the RSA-SHA1 signature method is defined in
+    `section 3.4.3`_ of RFC 5849.
+
+    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
+    """
+
+    # 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(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 _verify_rsa(hash_algorithm_name: str,
+                request,
+                rsa_public_key: str):
+    """
+    Verify a base64 encoded signature for a RSA-based signature method.
+
+    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.
+
+    Verification for the RSA-SHA1 signature method is defined in
+    `section 3.4.3`_ of RFC 5849.
+
+    .. _`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.
+
+        .. _`RFC2616 Sec 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
+    """
+
+    try:
+        # Calculate the *signature base string* of the actual received request
+
+        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)
+
+        # Obtain the signature that was received in the request
+
+        sig = binascii.a2b_base64(request.signature.encode('ascii'))
+
+        # Get the implementation of RSA-with-hash algorithm to use
+
+        alg = _get_jwt_rsa_algorithm(hash_algorithm_name)
+
+        # 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)
+
+        # 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.
+
+        verify_ok = alg.verify(sig_base_str.encode('ascii'), key, sig)
+
+        if not verify_ok:
+            log.debug('Verify failed: RSA with ' + alg.hash_alg.name +
+                      ': signature base string=%s' + sig_base_str)
+        return verify_ok
+
+    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
+
+
+# ==== 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
+
+    return _sign_rsa('SHA-1', sig_base_str, client.rsa_key)
+
+
+def verify_rsa_sha1(request, rsa_public_key: str):
+    return _verify_rsa('SHA-1', request, rsa_public_key)
+
+
+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.
+    """
+    warnings.warn('use _sign_rsa("SHA-1", ...) instead of sign_rsa_sha1',
+                  DeprecationWarning)
+
+    if isinstance(base_string, bytes):
+        base_string = base_string.decode('ascii')
+
+    return _sign_rsa('SHA-1', base_string, rsa_private_key)
+
+
+# ==== RSA-SHA256 ================================================
+
+def sign_rsa_sha256_with_client(sig_base_str: str, client):
+    return _sign_rsa('SHA-256', sig_base_str, client.rsa_key)
+
+
+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
+
+    """
+
+    # 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 '')
+
+    return signature
+
+
+def verify_plaintext(request, client_secret=None, resource_owner_secret=None):
+    """Verify a PLAINTEXT signature.
+
+    Per `section 3.4`_ of the spec.
+
+    .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
+    """
+    signature = sign_plaintext(client_secret, resource_owner_secret)
+    match = safe_string_equals(signature, request.signature)
+    if not match:
+        log.debug('Verify PLAINTEXT failed')
+    return match