Package oauth2client :: Module crypt |
[hide private]
[frames] | no frames]

# Source Code for Module oauth2client.crypt

      1  #!/usr/bin/python2.4
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright (C) 2011 Google Inc.
      5  #
      6  # Licensed under the Apache License, Version 2.0 (the "License");
      7  # you may not use this file except in compliance with the License.
      8  # You may obtain a copy of the License at
      9  #
     10  #      http://www.apache.org/licenses/LICENSE-2.0
     11  #
     12  # Unless required by applicable law or agreed to in writing, software
     13  # distributed under the License is distributed on an "AS IS" BASIS,
     14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15  # See the License for the specific language governing permissions and
     16  # limitations under the License.
     17
     18  import base64
     19  import hashlib
     20  import logging
     21  import time
     22
     23  from anyjson import simplejson
     24
     25
     26  CLOCK_SKEW_SECS = 300  # 5 minutes in seconds
     27  AUTH_TOKEN_LIFETIME_SECS = 300  # 5 minutes in seconds
     28  MAX_TOKEN_LIFETIME_SECS = 86400  # 1 day in seconds
     29
     30
     31  logger = logging.getLogger(__name__)
     32
     33
     34 -class AppIdentityError(Exception):
     35    pass
     36
     37
     38  try:
     39    from OpenSSL import crypto
     40
     41
     42 -  class OpenSSLVerifier(object):
     43      """Verifies the signature on a message."""
     44
     45 -    def __init__(self, pubkey):
     46        """Constructor.
     47
     48        Args:
     49          pubkey, OpenSSL.crypto.PKey, The public key to verify with.
     50        """
     51        self._pubkey = pubkey
     52
     53 -    def verify(self, message, signature):
     54        """Verifies a message against a signature.
     55
     56        Args:
     57          message: string, The message to verify.
     58          signature: string, The signature on the message.
     59
     60        Returns:
     61          True if message was signed by the private key associated with the public
     62          key that this object was constructed with.
     63        """
     64        try:
     65          crypto.verify(self._pubkey, signature, message, 'sha256')
     66          return True
     67        except:
     68          return False
     69
     70      @staticmethod
     71 -    def from_string(key_pem, is_x509_cert):
     72        """Construct a Verified instance from a string.
     73
     74        Args:
     75          key_pem: string, public key in PEM format.
     76          is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
     77            expected to be an RSA key in PEM format.
     78
     79        Returns:
     80          Verifier instance.
     81
     82        Raises:
     83          OpenSSL.crypto.Error if the key_pem can't be parsed.
     84        """
     85        if is_x509_cert:
     86          pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
     87        else:
     88          pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
     89        return OpenSSLVerifier(pubkey)
     90
     91
     92 -  class OpenSSLSigner(object):
     93      """Signs messages with a private key."""
     94
     95 -    def __init__(self, pkey):
     96        """Constructor.
     97
     98        Args:
     99          pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
    100        """
    101        self._key = pkey
    102
    103 -    def sign(self, message):
    104        """Signs a message.
    105
    106        Args:
    107          message: string, Message to be signed.
    108
    109        Returns:
    110          string, The signature of the message for the given key.
    111        """
    112        return crypto.sign(self._key, message, 'sha256')
    113
    114      @staticmethod
    115 -    def from_string(key, password='notasecret'):
    116        """Construct a Signer instance from a string.
    117
    118        Args:
    119          key: string, private key in PKCS12 or PEM format.
    120          password: string, password for the private key file.
    121
    122        Returns:
    123          Signer instance.
    124
    125        Raises:
    126          OpenSSL.crypto.Error if the key can't be parsed.
    127        """
    128        if key.startswith('-----BEGIN '):
    129          pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
    130        else:
    131          pkey = crypto.load_pkcs12(key, password).get_privatekey()
    132        return OpenSSLSigner(pkey)
    133
    134  except ImportError:
    135    OpenSSLVerifier = None
    136    OpenSSLSigner = None
    137
    138
    139  try:
    140    from Crypto.PublicKey import RSA
    141    from Crypto.Hash import SHA256
    142    from Crypto.Signature import PKCS1_v1_5
    143
    144
    145 -  class PyCryptoVerifier(object):
    146      """Verifies the signature on a message."""
    147
    148 -    def __init__(self, pubkey):
    149        """Constructor.
    150
    151        Args:
    152          pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with.
    153        """
    154        self._pubkey = pubkey
    155
    156 -    def verify(self, message, signature):
    157        """Verifies a message against a signature.
    158
    159        Args:
    160          message: string, The message to verify.
    161          signature: string, The signature on the message.
    162
    163        Returns:
    164          True if message was signed by the private key associated with the public
    165          key that this object was constructed with.
    166        """
    167        try:
    168          return PKCS1_v1_5.new(self._pubkey).verify(
    169              SHA256.new(message), signature)
    170        except:
    171          return False
    172
    173      @staticmethod
    174 -    def from_string(key_pem, is_x509_cert):
    175        """Construct a Verified instance from a string.
    176
    177        Args:
    178          key_pem: string, public key in PEM format.
    179          is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
    180            expected to be an RSA key in PEM format.
    181
    182        Returns:
    183          Verifier instance.
    184
    185        Raises:
    186          NotImplementedError if is_x509_cert is true.
    187        """
    188        if is_x509_cert:
    189          raise NotImplementedError(
    190              'X509 certs are not supported by the PyCrypto library. '
    191              'Try using PyOpenSSL if native code is an option.')
    192        else:
    193          pubkey = RSA.importKey(key_pem)
    194        return PyCryptoVerifier(pubkey)
    195
    196
    197 -  class PyCryptoSigner(object):
    198      """Signs messages with a private key."""
    199
    200 -    def __init__(self, pkey):
    201        """Constructor.
    202
    203        Args:
    204          pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
    205        """
    206        self._key = pkey
    207
    208 -    def sign(self, message):
    209        """Signs a message.
    210
    211        Args:
    212          message: string, Message to be signed.
    213
    214        Returns:
    215          string, The signature of the message for the given key.
    216        """
    217        return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
    218
    219      @staticmethod
    220 -    def from_string(key, password='notasecret'):
    221        """Construct a Signer instance from a string.
    222
    223        Args:
    224          key: string, private key in PEM format.
    225          password: string, password for private key file. Unused for PEM files.
    226
    227        Returns:
    228          Signer instance.
    229
    230        Raises:
    231          NotImplementedError if they key isn't in PEM format.
    232        """
    233        if key.startswith('-----BEGIN '):
    234          pkey = RSA.importKey(key)
    235        else:
    236          raise NotImplementedError(
    237              'PKCS12 format is not supported by the PyCrpto library. '
    238              'Try converting to a "PEM" '
    239              '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) '
    240              'or using PyOpenSSL if native code is an option.')
    241        return PyCryptoSigner(pkey)
    242
    243  except ImportError:
    244    PyCryptoVerifier = None
    245    PyCryptoSigner = None
    246
    247
    248  if OpenSSLSigner:
    249    Signer = OpenSSLSigner
    250    Verifier = OpenSSLVerifier
    251  elif PyCryptoSigner:
    252    Signer = PyCryptoSigner
    253    Verifier = PyCryptoVerifier
    254  else:
    255    raise ImportError('No encryption library found. Please install either '
    256                      'PyOpenSSL, or PyCrypto 2.6 or later')
    257
    258
    259 -def _urlsafe_b64encode(raw_bytes):
    260    return base64.urlsafe_b64encode(raw_bytes).rstrip('=')
    261
    262
    263 -def _urlsafe_b64decode(b64string):
    264    # Guard against unicode strings, which base64 can't handle.
    265    b64string = b64string.encode('ascii')
    266    padded = b64string + '=' * (4 - len(b64string) % 4)
    267    return base64.urlsafe_b64decode(padded)
    268
    269
    270 -def _json_encode(data):
    271    return simplejson.dumps(data, separators = (',', ':'))
    272
    273
    274 -def make_signed_jwt(signer, payload):
    275    """Make a signed JWT.
    276
    277    See http://self-issued.info/docs/draft-jones-json-web-token.html.
    278
    279    Args:
    280      signer: crypt.Signer, Cryptographic signer.
    281      payload: dict, Dictionary of data to convert to JSON and then sign.
    282
    283    Returns:
    284      string, The JWT for the payload.
    285    """
    286    header = {'typ': 'JWT', 'alg': 'RS256'}
    287
    288    segments = [
    289            _urlsafe_b64encode(_json_encode(header)),
    290            _urlsafe_b64encode(_json_encode(payload)),
    291    ]
    292    signing_input = '.'.join(segments)
    293
    294    signature = signer.sign(signing_input)
    295    segments.append(_urlsafe_b64encode(signature))
    296
    297    logger.debug(str(segments))
    298
    299    return '.'.join(segments)
    300
    301
    302 -def verify_signed_jwt_with_certs(jwt, certs, audience):
    303    """Verify a JWT against public certs.
    304
    305    See http://self-issued.info/docs/draft-jones-json-web-token.html.
    306
    307    Args:
    308      jwt: string, A JWT.
    309      certs: dict, Dictionary where values of public keys in PEM format.
    310      audience: string, The audience, 'aud', that this JWT should contain. If
    311        None then the JWT's 'aud' parameter is not verified.
    312
    313    Returns:
    314      dict, The deserialized JSON payload in the JWT.
    315
    316    Raises:
    317      AppIdentityError if any checks are failed.
    318    """
    319    segments = jwt.split('.')
    320
    321    if (len(segments) != 3):
    322      raise AppIdentityError(
    323        'Wrong number of segments in token: %s' % jwt)
    324    signed = '%s.%s' % (segments[0], segments[1])
    325
    326    signature = _urlsafe_b64decode(segments[2])
    327
    328    # Parse token.
    329    json_body = _urlsafe_b64decode(segments[1])
    330    try:
    331      parsed = simplejson.loads(json_body)
    332    except:
    333      raise AppIdentityError('Can\'t parse token: %s' % json_body)
    334
    335    # Check signature.
    336    verified = False
    337    for (keyname, pem) in certs.items():
    338      verifier = Verifier.from_string(pem, True)
    339      if (verifier.verify(signed, signature)):
    340        verified = True
    341        break
    342    if not verified:
    343      raise AppIdentityError('Invalid token signature: %s' % jwt)
    344
    345    # Check creation timestamp.
    346    iat = parsed.get('iat')
    347    if iat is None:
    348      raise AppIdentityError('No iat field in token: %s' % json_body)
    349    earliest = iat - CLOCK_SKEW_SECS
    350
    351    # Check expiration timestamp.
    352    now = long(time.time())
    353    exp = parsed.get('exp')
    354    if exp is None:
    355      raise AppIdentityError('No exp field in token: %s' % json_body)
    356    if exp >= now + MAX_TOKEN_LIFETIME_SECS:
    357      raise AppIdentityError(
    358        'exp field too far in future: %s' % json_body)
    359    latest = exp + CLOCK_SKEW_SECS
    360
    361    if now < earliest:
    362      raise AppIdentityError('Token used too early, %d < %d: %s' %
    363        (now, earliest, json_body))
    364    if now > latest:
    365      raise AppIdentityError('Token used too late, %d > %d: %s' %
    366        (now, latest, json_body))
    367
    368    # Check audience.
    369    if audience is not None:
    370      aud = parsed.get('aud')
    371      if aud is None:
    372        raise AppIdentityError('No aud field in token: %s' % json_body)
    373      if aud != audience:
    374        raise AppIdentityError('Wrong recipient, %s != %s: %s' %
    375            (aud, audience, json_body))
    376
    377    return parsed
    378