lepture/authlib

View on GitHub
authlib/oauth1/rfc5849/signature.py

Summary

Maintainability
A
0 mins
Test Coverage
"""
    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 = [f'{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)