123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- # -*- coding: utf-8 -*-
- """
- oauthlib.oauth1.rfc5849.signature
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 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
- 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
- .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
- """
- from __future__ import absolute_import, unicode_literals
- import binascii
- import hashlib
- import hmac
- try:
- import urlparse
- except ImportError:
- import urllib.parse as urlparse
- from . import utils
- from oauthlib.common import urldecode, extract_params, safe_string_equals
- from oauthlib.common import bytes_type, unicode_type
- def construct_base_string(http_method, base_string_uri,
- normalized_encoded_request_parameters):
- """**String Construction**
- Per `section 3.4.1.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="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
- c2&a3=2+q
- is represented by the following signature base string (line breaks
- are for display purposes only)::
- 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
- .. _`section 3.4.1.1`: http://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`: http://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`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
- # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
- base_string += utils.escape(base_string_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`).
- #
- # .. _`Section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
- # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
- base_string += utils.escape(normalized_encoded_request_parameters)
- return base_string
- def normalize_base_string_uri(uri, host=None):
- """**Base String URI**
- Per `section 3.4.1.2`_ of the spec.
- 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".
- In another example, the HTTPS request::
- GET /?q=1 HTTP/1.1
- Host: www.example.net:8080
- is represented by the base string URI: "https://www.example.net:8080/".
- .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
- The host argument overrides the netloc part of the uri argument.
- """
- if not isinstance(uri, unicode_type):
- raise ValueError('uri must be a unicode object.')
- # FIXME: urlparse does not support unicode
- scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
- # 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`: http://tools.ietf.org/html/rfc3986
- if not scheme or not netloc:
- raise ValueError('uri must include a scheme and netloc')
- # 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`: http://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()
- # 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()
- # 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`: http://tools.ietf.org/html/rfc2616
- # .. _`RFC2818`: http://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
- return urlparse.urlunparse((scheme, netloc, path, params, '', ''))
- # ** 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`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
- def collect_parameters(uri_query='', body=[], headers=None,
- exclude_oauth_signature=True, with_realm=False):
- """**Parameter Sources**
- 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.
- 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`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
- """
- 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`_, Section 17.13.4.
- #
- # .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4
- # .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.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`: http://tools.ietf.org/html/rfc5849#section-3.5.1
- if headers:
- headers_lower = dict((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`_.
- # * The HTTP request entity-header includes the "Content-Type"
- # header field set to "application/x-www-form-urlencoded".
- #
- # .._`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.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):
- """**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`: http://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`: http://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`: http://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 = ['{0}={1}'.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)
- def sign_hmac_sha1_with_client(base_string, client):
- return sign_hmac_sha1(base_string,
- client.client_secret,
- client.resource_owner_secret
- )
- 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`_::
- digest = HMAC-SHA1 (key, text)
- Per `section 3.4.2`_ of the spec.
- .. _`RFC2104`: http://tools.ietf.org/html/rfc2104
- .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2
- """
- # The HMAC-SHA1 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`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
- text = base_string
- # key is set to the concatenated values of:
- # 1. The client shared-secret, after being encoded (`Section 3.6`_).
- #
- # .. _`Section 3.6`: http://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`: http://tools.ietf.org/html/rfc5849#section-3.6
- key += utils.escape(resource_owner_secret or '')
- # 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)
- # 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`: http://tools.ietf.org/html/rfc2045#section-6.8
- return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
- _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_sha1(base_string, rsa_private_key):
- """**RSA-SHA1**
- Per `section 3.4.3`_ of the spec.
- 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
- 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`: http://tools.ietf.org/html/rfc5849#section-3.4.3
- .. _`RFC3447, Section 8.2`: http://tools.ietf.org/html/rfc3447#section-8.2
- """
- if isinstance(base_string, unicode_type):
- base_string = base_string.encode('utf-8')
- # TODO: finish RSA documentation
- alg = _jwt_rs1_signing_algorithm()
- key = _prepare_key_plus(alg, rsa_private_key)
- s=alg.sign(base_string, key)
- return binascii.b2a_base64(s)[:-1].decode('utf-8')
- 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 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`: http://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`: http://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`: http://tools.ietf.org/html/rfc5849#section-3.6
- signature += utils.escape(resource_owner_secret or '')
- return signature
- def sign_plaintext_with_client(base_string, client):
- return sign_plaintext(client.client_secret, client.resource_owner_secret)
- def verify_hmac_sha1(request, client_secret=None,
- resource_owner_secret=None):
- """Verify a HMAC-SHA1 signature.
- Per `section 3.4`_ of the spec.
- .. _`section 3.4`: http://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`: http://tools.ietf.org/html/rfc2616#section-5.2
- """
- norm_params = normalize_parameters(request.params)
- uri = normalize_base_string_uri(request.uri)
- base_string = construct_base_string(request.http_method, uri, norm_params)
- signature = sign_hmac_sha1(base_string, client_secret,
- resource_owner_secret)
- return safe_string_equals(signature, request.signature)
- def _prepare_key_plus(alg, keystr):
- if isinstance(keystr, bytes_type):
- keystr = keystr.decode('utf-8')
- return alg.prepare_key(keystr)
- 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.
- Note this method requires the jwt and cryptography libraries.
- .. _`section 3.4.3`: http://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 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2
- """
- norm_params = normalize_parameters(request.params)
- uri = normalize_base_string_uri(request.uri)
- message = construct_base_string(request.http_method, 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)
- return alg.verify(message, key, sig)
- 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`: http://tools.ietf.org/html/rfc5849#section-3.4
- """
- signature = sign_plaintext(client_secret, resource_owner_secret)
- return safe_string_equals(signature, request.signature)
|