123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852 |
- """
- 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
|