diff options
author | Asif Saif Uddin <auvipy@gmail.com> | 2020-06-06 08:07:20 +0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-06 08:07:20 +0600 |
commit | 72269341361ef50b1bcd920740bdb1983f6a7337 (patch) | |
tree | 9362bb4efa6f849956ddf44b5a701c0fc8f8c7b2 /oauthlib/oauth1/rfc5849/signature.py | |
parent | dc4d464bc83588d345e021398618fc1da2705fe1 (diff) | |
parent | bda81b3cb6306dec19a6e60113e21b2933d0950c (diff) | |
download | oauthlib-doc-dynreg.tar.gz |
Merge branch 'master' into doc-dynregdoc-dynreg
Diffstat (limited to 'oauthlib/oauth1/rfc5849/signature.py')
-rw-r--r-- | oauthlib/oauth1/rfc5849/signature.py | 874 |
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): |