Home | History | Annotate | Download | only in oauth2client
      1 # -*- coding: utf-8 -*-
      2 #
      3 # Copyright 2014 Google Inc. All rights reserved.
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 """Crypto-related routines for oauth2client."""
     17 
     18 import json
     19 import logging
     20 import time
     21 
     22 from oauth2client import _helpers
     23 from oauth2client import _pure_python_crypt
     24 
     25 
     26 RsaSigner = _pure_python_crypt.RsaSigner
     27 RsaVerifier = _pure_python_crypt.RsaVerifier
     28 
     29 CLOCK_SKEW_SECS = 300  # 5 minutes in seconds
     30 AUTH_TOKEN_LIFETIME_SECS = 300  # 5 minutes in seconds
     31 MAX_TOKEN_LIFETIME_SECS = 86400  # 1 day in seconds
     32 
     33 logger = logging.getLogger(__name__)
     34 
     35 
     36 class AppIdentityError(Exception):
     37     """Error to indicate crypto failure."""
     38 
     39 
     40 def _bad_pkcs12_key_as_pem(*args, **kwargs):
     41     raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
     42 
     43 
     44 try:
     45     from oauth2client import _openssl_crypt
     46     OpenSSLSigner = _openssl_crypt.OpenSSLSigner
     47     OpenSSLVerifier = _openssl_crypt.OpenSSLVerifier
     48     pkcs12_key_as_pem = _openssl_crypt.pkcs12_key_as_pem
     49 except ImportError:  # pragma: NO COVER
     50     OpenSSLVerifier = None
     51     OpenSSLSigner = None
     52     pkcs12_key_as_pem = _bad_pkcs12_key_as_pem
     53 
     54 try:
     55     from oauth2client import _pycrypto_crypt
     56     PyCryptoSigner = _pycrypto_crypt.PyCryptoSigner
     57     PyCryptoVerifier = _pycrypto_crypt.PyCryptoVerifier
     58 except ImportError:  # pragma: NO COVER
     59     PyCryptoVerifier = None
     60     PyCryptoSigner = None
     61 
     62 
     63 if OpenSSLSigner:
     64     Signer = OpenSSLSigner
     65     Verifier = OpenSSLVerifier
     66 elif PyCryptoSigner:  # pragma: NO COVER
     67     Signer = PyCryptoSigner
     68     Verifier = PyCryptoVerifier
     69 else:  # pragma: NO COVER
     70     Signer = RsaSigner
     71     Verifier = RsaVerifier
     72 
     73 
     74 def make_signed_jwt(signer, payload, key_id=None):
     75     """Make a signed JWT.
     76 
     77     See http://self-issued.info/docs/draft-jones-json-web-token.html.
     78 
     79     Args:
     80         signer: crypt.Signer, Cryptographic signer.
     81         payload: dict, Dictionary of data to convert to JSON and then sign.
     82         key_id: string, (Optional) Key ID header.
     83 
     84     Returns:
     85         string, The JWT for the payload.
     86     """
     87     header = {'typ': 'JWT', 'alg': 'RS256'}
     88     if key_id is not None:
     89         header['kid'] = key_id
     90 
     91     segments = [
     92         _helpers._urlsafe_b64encode(_helpers._json_encode(header)),
     93         _helpers._urlsafe_b64encode(_helpers._json_encode(payload)),
     94     ]
     95     signing_input = b'.'.join(segments)
     96 
     97     signature = signer.sign(signing_input)
     98     segments.append(_helpers._urlsafe_b64encode(signature))
     99 
    100     logger.debug(str(segments))
    101 
    102     return b'.'.join(segments)
    103 
    104 
    105 def _verify_signature(message, signature, certs):
    106     """Verifies signed content using a list of certificates.
    107 
    108     Args:
    109         message: string or bytes, The message to verify.
    110         signature: string or bytes, The signature on the message.
    111         certs: iterable, certificates in PEM format.
    112 
    113     Raises:
    114         AppIdentityError: If none of the certificates can verify the message
    115                           against the signature.
    116     """
    117     for pem in certs:
    118         verifier = Verifier.from_string(pem, is_x509_cert=True)
    119         if verifier.verify(message, signature):
    120             return
    121 
    122     # If we have not returned, no certificate confirms the signature.
    123     raise AppIdentityError('Invalid token signature')
    124 
    125 
    126 def _check_audience(payload_dict, audience):
    127     """Checks audience field from a JWT payload.
    128 
    129     Does nothing if the passed in ``audience`` is null.
    130 
    131     Args:
    132         payload_dict: dict, A dictionary containing a JWT payload.
    133         audience: string or NoneType, an audience to check for in
    134                   the JWT payload.
    135 
    136     Raises:
    137         AppIdentityError: If there is no ``'aud'`` field in the payload
    138                           dictionary but there is an ``audience`` to check.
    139         AppIdentityError: If the ``'aud'`` field in the payload dictionary
    140                           does not match the ``audience``.
    141     """
    142     if audience is None:
    143         return
    144 
    145     audience_in_payload = payload_dict.get('aud')
    146     if audience_in_payload is None:
    147         raise AppIdentityError(
    148             'No aud field in token: {0}'.format(payload_dict))
    149     if audience_in_payload != audience:
    150         raise AppIdentityError('Wrong recipient, {0} != {1}: {2}'.format(
    151             audience_in_payload, audience, payload_dict))
    152 
    153 
    154 def _verify_time_range(payload_dict):
    155     """Verifies the issued at and expiration from a JWT payload.
    156 
    157     Makes sure the current time (in UTC) falls between the issued at and
    158     expiration for the JWT (with some skew allowed for via
    159     ``CLOCK_SKEW_SECS``).
    160 
    161     Args:
    162         payload_dict: dict, A dictionary containing a JWT payload.
    163 
    164     Raises:
    165         AppIdentityError: If there is no ``'iat'`` field in the payload
    166                           dictionary.
    167         AppIdentityError: If there is no ``'exp'`` field in the payload
    168                           dictionary.
    169         AppIdentityError: If the JWT expiration is too far in the future (i.e.
    170                           if the expiration would imply a token lifetime
    171                           longer than what is allowed.)
    172         AppIdentityError: If the token appears to have been issued in the
    173                           future (up to clock skew).
    174         AppIdentityError: If the token appears to have expired in the past
    175                           (up to clock skew).
    176     """
    177     # Get the current time to use throughout.
    178     now = int(time.time())
    179 
    180     # Make sure issued at and expiration are in the payload.
    181     issued_at = payload_dict.get('iat')
    182     if issued_at is None:
    183         raise AppIdentityError(
    184             'No iat field in token: {0}'.format(payload_dict))
    185     expiration = payload_dict.get('exp')
    186     if expiration is None:
    187         raise AppIdentityError(
    188             'No exp field in token: {0}'.format(payload_dict))
    189 
    190     # Make sure the expiration gives an acceptable token lifetime.
    191     if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
    192         raise AppIdentityError(
    193             'exp field too far in future: {0}'.format(payload_dict))
    194 
    195     # Make sure (up to clock skew) that the token wasn't issued in the future.
    196     earliest = issued_at - CLOCK_SKEW_SECS
    197     if now < earliest:
    198         raise AppIdentityError('Token used too early, {0} < {1}: {2}'.format(
    199             now, earliest, payload_dict))
    200     # Make sure (up to clock skew) that the token isn't already expired.
    201     latest = expiration + CLOCK_SKEW_SECS
    202     if now > latest:
    203         raise AppIdentityError('Token used too late, {0} > {1}: {2}'.format(
    204             now, latest, payload_dict))
    205 
    206 
    207 def verify_signed_jwt_with_certs(jwt, certs, audience=None):
    208     """Verify a JWT against public certs.
    209 
    210     See http://self-issued.info/docs/draft-jones-json-web-token.html.
    211 
    212     Args:
    213         jwt: string, A JWT.
    214         certs: dict, Dictionary where values of public keys in PEM format.
    215         audience: string, The audience, 'aud', that this JWT should contain. If
    216                   None then the JWT's 'aud' parameter is not verified.
    217 
    218     Returns:
    219         dict, The deserialized JSON payload in the JWT.
    220 
    221     Raises:
    222         AppIdentityError: if any checks are failed.
    223     """
    224     jwt = _helpers._to_bytes(jwt)
    225 
    226     if jwt.count(b'.') != 2:
    227         raise AppIdentityError(
    228             'Wrong number of segments in token: {0}'.format(jwt))
    229 
    230     header, payload, signature = jwt.split(b'.')
    231     message_to_sign = header + b'.' + payload
    232     signature = _helpers._urlsafe_b64decode(signature)
    233 
    234     # Parse token.
    235     payload_bytes = _helpers._urlsafe_b64decode(payload)
    236     try:
    237         payload_dict = json.loads(_helpers._from_bytes(payload_bytes))
    238     except:
    239         raise AppIdentityError('Can\'t parse token: {0}'.format(payload_bytes))
    240 
    241     # Verify that the signature matches the message.
    242     _verify_signature(message_to_sign, signature, certs.values())
    243 
    244     # Verify the issued at and created times in the payload.
    245     _verify_time_range(payload_dict)
    246 
    247     # Check audience.
    248     _check_audience(payload_dict, audience)
    249 
    250     return payload_dict
    251