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 base64
     19 import imp
     20 import json
     21 import logging
     22 import os
     23 import sys
     24 import time
     25 
     26 import six
     27 
     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 
     34 logger = logging.getLogger(__name__)
     35 
     36 
     37 class AppIdentityError(Exception):
     38   pass
     39 
     40 
     41 def _TryOpenSslImport():
     42   """Import OpenSSL, avoiding the explicit import where possible.
     43 
     44   Importing OpenSSL 0.14 can take up to 0.5s, which is a large price
     45   to pay at module import time. However, it's also possible for
     46   ``imp.find_module`` to fail to find the module, even when it's
     47   installed. (This is the case in various exotic environments,
     48   including some relevant for Google.) So we first try a fast-path,
     49   and fall back to the slow import as needed.
     50 
     51   Args:
     52     None
     53   Returns:
     54     None
     55   Raises:
     56     ImportError if OpenSSL is unavailable.
     57 
     58   """
     59   try:
     60     _ = imp.find_module('OpenSSL')
     61     return
     62   except ImportError:
     63     import OpenSSL
     64 
     65 
     66 try:
     67   _TryOpenSslImport()
     68 
     69   class OpenSSLVerifier(object):
     70     """Verifies the signature on a message."""
     71 
     72     def __init__(self, pubkey):
     73       """Constructor.
     74 
     75       Args:
     76         pubkey, OpenSSL.crypto.PKey, The public key to verify with.
     77       """
     78       self._pubkey = pubkey
     79 
     80     def verify(self, message, signature):
     81       """Verifies a message against a signature.
     82 
     83       Args:
     84         message: string, The message to verify.
     85         signature: string, The signature on the message.
     86 
     87       Returns:
     88         True if message was signed by the private key associated with the public
     89         key that this object was constructed with.
     90       """
     91       from OpenSSL import crypto
     92       try:
     93         if isinstance(message, six.text_type):
     94           message = message.encode('utf-8')
     95         crypto.verify(self._pubkey, signature, message, 'sha256')
     96         return True
     97       except:
     98         return False
     99 
    100     @staticmethod
    101     def from_string(key_pem, is_x509_cert):
    102       """Construct a Verified instance from a string.
    103 
    104       Args:
    105         key_pem: string, public key in PEM format.
    106         is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
    107           expected to be an RSA key in PEM format.
    108 
    109       Returns:
    110         Verifier instance.
    111 
    112       Raises:
    113         OpenSSL.crypto.Error if the key_pem can't be parsed.
    114       """
    115       from OpenSSL import crypto
    116       if is_x509_cert:
    117         pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
    118       else:
    119         pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
    120       return OpenSSLVerifier(pubkey)
    121 
    122 
    123   class OpenSSLSigner(object):
    124     """Signs messages with a private key."""
    125 
    126     def __init__(self, pkey):
    127       """Constructor.
    128 
    129       Args:
    130         pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
    131       """
    132       self._key = pkey
    133 
    134     def sign(self, message):
    135       """Signs a message.
    136 
    137       Args:
    138         message: bytes, Message to be signed.
    139 
    140       Returns:
    141         string, The signature of the message for the given key.
    142       """
    143       from OpenSSL import crypto
    144       if isinstance(message, six.text_type):
    145         message = message.encode('utf-8')
    146       return crypto.sign(self._key, message, 'sha256')
    147 
    148     @staticmethod
    149     def from_string(key, password=b'notasecret'):
    150       """Construct a Signer instance from a string.
    151 
    152       Args:
    153         key: string, private key in PKCS12 or PEM format.
    154         password: string, password for the private key file.
    155 
    156       Returns:
    157         Signer instance.
    158 
    159       Raises:
    160         OpenSSL.crypto.Error if the key can't be parsed.
    161       """
    162       from OpenSSL import crypto
    163       parsed_pem_key = _parse_pem_key(key)
    164       if parsed_pem_key:
    165         pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
    166       else:
    167         if isinstance(password, six.text_type):
    168           password = password.encode('utf-8')
    169         pkey = crypto.load_pkcs12(key, password).get_privatekey()
    170       return OpenSSLSigner(pkey)
    171 
    172 
    173   def pkcs12_key_as_pem(private_key_text, private_key_password):
    174     """Convert the contents of a PKCS12 key to PEM using OpenSSL.
    175 
    176     Args:
    177       private_key_text: String. Private key.
    178       private_key_password: String. Password for PKCS12.
    179 
    180     Returns:
    181       String. PEM contents of ``private_key_text``.
    182     """
    183     from OpenSSL import crypto
    184     decoded_body = base64.b64decode(private_key_text)
    185     if isinstance(private_key_password, six.string_types):
    186       private_key_password = private_key_password.encode('ascii')
    187 
    188     pkcs12 = crypto.load_pkcs12(decoded_body, private_key_password)
    189     return crypto.dump_privatekey(crypto.FILETYPE_PEM,
    190                                   pkcs12.get_privatekey())
    191 except ImportError:
    192   OpenSSLVerifier = None
    193   OpenSSLSigner = None
    194   def pkcs12_key_as_pem(*args, **kwargs):
    195     raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
    196 
    197 
    198 try:
    199   from Crypto.PublicKey import RSA
    200   from Crypto.Hash import SHA256
    201   from Crypto.Signature import PKCS1_v1_5
    202   from Crypto.Util.asn1 import DerSequence
    203 
    204 
    205   class PyCryptoVerifier(object):
    206     """Verifies the signature on a message."""
    207 
    208     def __init__(self, pubkey):
    209       """Constructor.
    210 
    211       Args:
    212         pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with.
    213       """
    214       self._pubkey = pubkey
    215 
    216     def verify(self, message, signature):
    217       """Verifies a message against a signature.
    218 
    219       Args:
    220         message: string, The message to verify.
    221         signature: string, The signature on the message.
    222 
    223       Returns:
    224         True if message was signed by the private key associated with the public
    225         key that this object was constructed with.
    226       """
    227       try:
    228         return PKCS1_v1_5.new(self._pubkey).verify(
    229             SHA256.new(message), signature)
    230       except:
    231         return False
    232 
    233     @staticmethod
    234     def from_string(key_pem, is_x509_cert):
    235       """Construct a Verified instance from a string.
    236 
    237       Args:
    238         key_pem: string, public key in PEM format.
    239         is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
    240           expected to be an RSA key in PEM format.
    241 
    242       Returns:
    243         Verifier instance.
    244       """
    245       if is_x509_cert:
    246         if isinstance(key_pem, six.text_type):
    247           key_pem = key_pem.encode('ascii')
    248         pemLines = key_pem.replace(b' ', b'').split()
    249         certDer = _urlsafe_b64decode(b''.join(pemLines[1:-1]))
    250         certSeq = DerSequence()
    251         certSeq.decode(certDer)
    252         tbsSeq = DerSequence()
    253         tbsSeq.decode(certSeq[0])
    254         pubkey = RSA.importKey(tbsSeq[6])
    255       else:
    256         pubkey = RSA.importKey(key_pem)
    257       return PyCryptoVerifier(pubkey)
    258 
    259 
    260   class PyCryptoSigner(object):
    261     """Signs messages with a private key."""
    262 
    263     def __init__(self, pkey):
    264       """Constructor.
    265 
    266       Args:
    267         pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
    268       """
    269       self._key = pkey
    270 
    271     def sign(self, message):
    272       """Signs a message.
    273 
    274       Args:
    275         message: string, Message to be signed.
    276 
    277       Returns:
    278         string, The signature of the message for the given key.
    279       """
    280       if isinstance(message, six.text_type):
    281         message = message.encode('utf-8')
    282       return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
    283 
    284     @staticmethod
    285     def from_string(key, password='notasecret'):
    286       """Construct a Signer instance from a string.
    287 
    288       Args:
    289         key: string, private key in PEM format.
    290         password: string, password for private key file. Unused for PEM files.
    291 
    292       Returns:
    293         Signer instance.
    294 
    295       Raises:
    296         NotImplementedError if they key isn't in PEM format.
    297       """
    298       parsed_pem_key = _parse_pem_key(key)
    299       if parsed_pem_key:
    300         pkey = RSA.importKey(parsed_pem_key)
    301       else:
    302         raise NotImplementedError(
    303             'PKCS12 format is not supported by the PyCrypto library. '
    304             'Try converting to a "PEM" '
    305             '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) '
    306             'or using PyOpenSSL if native code is an option.')
    307       return PyCryptoSigner(pkey)
    308 
    309 except ImportError:
    310   PyCryptoVerifier = None
    311   PyCryptoSigner = None
    312 
    313 
    314 if OpenSSLSigner:
    315   Signer = OpenSSLSigner
    316   Verifier = OpenSSLVerifier
    317 elif PyCryptoSigner:
    318   Signer = PyCryptoSigner
    319   Verifier = PyCryptoVerifier
    320 else:
    321   raise ImportError('No encryption library found. Please install either '
    322                     'PyOpenSSL, or PyCrypto 2.6 or later')
    323 
    324 
    325 def _parse_pem_key(raw_key_input):
    326   """Identify and extract PEM keys.
    327 
    328   Determines whether the given key is in the format of PEM key, and extracts
    329   the relevant part of the key if it is.
    330 
    331   Args:
    332     raw_key_input: The contents of a private key file (either PEM or PKCS12).
    333 
    334   Returns:
    335     string, The actual key if the contents are from a PEM file, or else None.
    336   """
    337   offset = raw_key_input.find(b'-----BEGIN ')
    338   if offset != -1:
    339     return raw_key_input[offset:]
    340 
    341 
    342 def _urlsafe_b64encode(raw_bytes):
    343   if isinstance(raw_bytes, six.text_type):
    344     raw_bytes = raw_bytes.encode('utf-8')
    345   return base64.urlsafe_b64encode(raw_bytes).decode('ascii').rstrip('=')
    346 
    347 
    348 def _urlsafe_b64decode(b64string):
    349   # Guard against unicode strings, which base64 can't handle.
    350   if isinstance(b64string, six.text_type):
    351     b64string = b64string.encode('ascii')
    352   padded = b64string + b'=' * (4 - len(b64string) % 4)
    353   return base64.urlsafe_b64decode(padded)
    354 
    355 
    356 def _json_encode(data):
    357   return json.dumps(data, separators=(',', ':'))
    358 
    359 
    360 def make_signed_jwt(signer, payload):
    361   """Make a signed JWT.
    362 
    363   See http://self-issued.info/docs/draft-jones-json-web-token.html.
    364 
    365   Args:
    366     signer: crypt.Signer, Cryptographic signer.
    367     payload: dict, Dictionary of data to convert to JSON and then sign.
    368 
    369   Returns:
    370     string, The JWT for the payload.
    371   """
    372   header = {'typ': 'JWT', 'alg': 'RS256'}
    373 
    374   segments = [
    375       _urlsafe_b64encode(_json_encode(header)),
    376       _urlsafe_b64encode(_json_encode(payload)),
    377   ]
    378   signing_input = '.'.join(segments)
    379 
    380   signature = signer.sign(signing_input)
    381   segments.append(_urlsafe_b64encode(signature))
    382 
    383   logger.debug(str(segments))
    384 
    385   return '.'.join(segments)
    386 
    387 
    388 def verify_signed_jwt_with_certs(jwt, certs, audience):
    389   """Verify a JWT against public certs.
    390 
    391   See http://self-issued.info/docs/draft-jones-json-web-token.html.
    392 
    393   Args:
    394     jwt: string, A JWT.
    395     certs: dict, Dictionary where values of public keys in PEM format.
    396     audience: string, The audience, 'aud', that this JWT should contain. If
    397       None then the JWT's 'aud' parameter is not verified.
    398 
    399   Returns:
    400     dict, The deserialized JSON payload in the JWT.
    401 
    402   Raises:
    403     AppIdentityError if any checks are failed.
    404   """
    405   segments = jwt.split('.')
    406 
    407   if len(segments) != 3:
    408     raise AppIdentityError('Wrong number of segments in token: %s' % jwt)
    409   signed = '%s.%s' % (segments[0], segments[1])
    410 
    411   signature = _urlsafe_b64decode(segments[2])
    412 
    413   # Parse token.
    414   json_body = _urlsafe_b64decode(segments[1])
    415   try:
    416     parsed = json.loads(json_body.decode('utf-8'))
    417   except:
    418     raise AppIdentityError('Can\'t parse token: %s' % json_body)
    419 
    420   # Check signature.
    421   verified = False
    422   for pem in certs.values():
    423     verifier = Verifier.from_string(pem, True)
    424     if verifier.verify(signed, signature):
    425       verified = True
    426       break
    427   if not verified:
    428     raise AppIdentityError('Invalid token signature: %s' % jwt)
    429 
    430   # Check creation timestamp.
    431   iat = parsed.get('iat')
    432   if iat is None:
    433     raise AppIdentityError('No iat field in token: %s' % json_body)
    434   earliest = iat - CLOCK_SKEW_SECS
    435 
    436   # Check expiration timestamp.
    437   now = int(time.time())
    438   exp = parsed.get('exp')
    439   if exp is None:
    440     raise AppIdentityError('No exp field in token: %s' % json_body)
    441   if exp >= now + MAX_TOKEN_LIFETIME_SECS:
    442     raise AppIdentityError('exp field too far in future: %s' % json_body)
    443   latest = exp + CLOCK_SKEW_SECS
    444 
    445   if now < earliest:
    446     raise AppIdentityError('Token used too early, %d < %d: %s' %
    447                            (now, earliest, json_body))
    448   if now > latest:
    449     raise AppIdentityError('Token used too late, %d > %d: %s' %
    450                            (now, latest, json_body))
    451 
    452   # Check audience.
    453   if audience is not None:
    454     aud = parsed.get('aud')
    455     if aud is None:
    456       raise AppIdentityError('No aud field in token: %s' % json_body)
    457     if aud != audience:
    458       raise AppIdentityError('Wrong recipient, %s != %s: %s' %
    459                              (aud, audience, json_body))
    460 
    461   return parsed
    462