# -*- coding: utf-8 -*- """ authlib.oauth1.rfc5849.signature ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This module represents a direct implementation of `section 3.4`_ of the spec. .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 """ import binascii import hashlib import hmac from authlib.common.urls import urlparse from authlib.common.encoding import to_unicode, to_bytes from .util import escape, unescape SIGNATURE_HMAC_SHA1 = "HMAC-SHA1" SIGNATURE_RSA_SHA1 = "RSA-SHA1" SIGNATURE_PLAINTEXT = "PLAINTEXT" SIGNATURE_TYPE_HEADER = 'HEADER' SIGNATURE_TYPE_QUERY = 'QUERY' SIGNATURE_TYPE_BODY = 'BODY' def construct_base_string(method, uri, params, host=None): """Generate signature base string from request, per `Section 3.4.1`_. 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`: https://tools.ietf.org/html/rfc5849#section-3.4.1 """ # Create base string URI per Section 3.4.1.2 base_string_uri = normalize_base_string_uri(uri, host) # Cleanup parameter sources per Section 3.4.1.3.1 unescaped_params = [] for k, v in params: # The "oauth_signature" parameter MUST be excluded from the signature if k in ('oauth_signature', 'realm'): continue # ensure oauth params are unescaped if k.startswith('oauth_'): v = unescape(v) unescaped_params.append((k, v)) # Normalize parameters per Section 3.4.1.3.2 normalized_params = normalize_parameters(unescaped_params) # construct base string return '&'.join([ escape(method.upper()), escape(base_string_uri), escape(normalized_params), ]) def normalize_base_string_uri(uri, host=None): """Normalize Base String URI per `Section 3.4.1.2`_. 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`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 The host argument overrides the netloc part of the uri argument. """ uri = to_unicode(uri) 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`: https://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`: 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() # 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`: 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 return urlparse.urlunparse((scheme, netloc, path, params, '', '')) def normalize_parameters(params): """Normalize parameters per `Section 3.4.1.3.2`_. 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 """ # 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 = [(escape(k), 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 generate_signature_base_string(request): """Generate signature base string from request.""" host = request.headers.get('Host', None) return construct_base_string( request.method, request.uri, request.params, host) def hmac_sha1_signature(base_string, client_secret, token_secret): """Generate signature via HMAC-SHA1 method, per `Section 3.4.2`_. The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature algorithm as defined in `RFC2104`_:: digest = HMAC-SHA1 (key, text) .. _`RFC2104`: https://tools.ietf.org/html/rfc2104 .. _`Section 3.4.2`: https://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`: https://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`: https://tools.ietf.org/html/rfc5849#section-3.6 key = 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 += escape(token_secret or '') signature = hmac.new(to_bytes(key), to_bytes(text), 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`: https://tools.ietf.org/html/rfc2045#section-6.8 sig = binascii.b2a_base64(signature.digest())[:-1] return to_unicode(sig) def rsa_sha1_signature(base_string, rsa_private_key): """Generate signature via RSA-SHA1 method, per `Section 3.4.3`_. 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`: https://tools.ietf.org/html/rfc5849#section-3.4.3 .. _`RFC3447, Section 8.2`: https://tools.ietf.org/html/rfc3447#section-8.2 """ from .rsa import sign_sha1 base_string = to_bytes(base_string) s = sign_sha1(to_bytes(base_string), rsa_private_key) sig = binascii.b2a_base64(s)[:-1] return to_unicode(sig) def plaintext_signature(client_secret, token_secret): """Generate signature via PLAINTEXT method, per `Section 3.4.4`_. 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 = 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 += escape(token_secret or '') return signature def sign_hmac_sha1(client, request): """Sign a HMAC-SHA1 signature.""" base_string = generate_signature_base_string(request) return hmac_sha1_signature( base_string, client.client_secret, client.token_secret) def sign_rsa_sha1(client, request): """Sign a RSASSA-PKCS #1 v1.5 base64 encoded signature.""" base_string = generate_signature_base_string(request) return rsa_sha1_signature(base_string, client.rsa_key) def sign_plaintext(client, request): """Sign a PLAINTEXT signature.""" return plaintext_signature(client.client_secret, client.token_secret) def verify_hmac_sha1(request): """Verify a HMAC-SHA1 signature.""" base_string = generate_signature_base_string(request) sig = hmac_sha1_signature( base_string, request.client_secret, request.token_secret) return hmac.compare_digest(sig, request.signature) def verify_rsa_sha1(request): """Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature.""" from .rsa import verify_sha1 base_string = generate_signature_base_string(request) sig = binascii.a2b_base64(to_bytes(request.signature)) return verify_sha1(sig, to_bytes(base_string), request.rsa_public_key) def verify_plaintext(request): """Verify a PLAINTEXT signature.""" sig = plaintext_signature(request.client_secret, request.token_secret) return hmac.compare_digest(sig, request.signature)