| OLD | NEW | 
|---|
| 1 #!/usr/bin/python2.4 |  | 
| 2 # -*- coding: utf-8 -*- | 1 # -*- coding: utf-8 -*- | 
| 3 # | 2 # | 
| 4 # Copyright (C) 2011 Google Inc. | 3 # Copyright 2014 Google Inc. All rights reserved. | 
| 5 # | 4 # | 
| 6 # Licensed under the Apache License, Version 2.0 (the "License"); | 5 # Licensed under the Apache License, Version 2.0 (the "License"); | 
| 7 # you may not use this file except in compliance with the License. | 6 # you may not use this file except in compliance with the License. | 
| 8 # You may obtain a copy of the License at | 7 # You may obtain a copy of the License at | 
| 9 # | 8 # | 
| 10 #      http://www.apache.org/licenses/LICENSE-2.0 | 9 #      http://www.apache.org/licenses/LICENSE-2.0 | 
| 11 # | 10 # | 
| 12 # Unless required by applicable law or agreed to in writing, software | 11 # Unless required by applicable law or agreed to in writing, software | 
| 13 # distributed under the License is distributed on an "AS IS" BASIS, | 12 # distributed under the License is distributed on an "AS IS" BASIS, | 
| 14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
| 15 # See the License for the specific language governing permissions and | 14 # See the License for the specific language governing permissions and | 
| 16 # limitations under the License. | 15 # limitations under the License. | 
|  | 16 """Crypto-related routines for oauth2client.""" | 
| 17 | 17 | 
| 18 import base64 | 18 import imp | 
| 19 import hashlib | 19 import json | 
| 20 import logging | 20 import logging | 
|  | 21 import os | 
| 21 import time | 22 import time | 
| 22 | 23 | 
| 23 from OpenSSL import crypto | 24 from oauth2client._helpers import _json_encode | 
| 24 from anyjson import simplejson | 25 from oauth2client._helpers import _urlsafe_b64decode | 
|  | 26 from oauth2client._helpers import _urlsafe_b64encode | 
| 25 | 27 | 
| 26 | 28 | 
| 27 CLOCK_SKEW_SECS = 300  # 5 minutes in seconds | 29 CLOCK_SKEW_SECS = 300  # 5 minutes in seconds | 
| 28 AUTH_TOKEN_LIFETIME_SECS = 300  # 5 minutes in seconds | 30 AUTH_TOKEN_LIFETIME_SECS = 300  # 5 minutes in seconds | 
| 29 MAX_TOKEN_LIFETIME_SECS = 86400  # 1 day in seconds | 31 MAX_TOKEN_LIFETIME_SECS = 86400  # 1 day in seconds | 
| 30 | 32 | 
| 31 | 33 | 
|  | 34 logger = logging.getLogger(__name__) | 
|  | 35 | 
|  | 36 | 
| 32 class AppIdentityError(Exception): | 37 class AppIdentityError(Exception): | 
| 33   pass | 38   pass | 
| 34 | 39 | 
| 35 | 40 | 
| 36 class Verifier(object): | 41 def _TryOpenSslImport(): | 
| 37   """Verifies the signature on a message.""" | 42   """Import OpenSSL, avoiding the explicit import where possible. | 
| 38 | 43 | 
| 39   def __init__(self, pubkey): | 44   Importing OpenSSL 0.14 can take up to 0.5s, which is a large price | 
| 40     """Constructor. | 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. | 
| 41 | 50 | 
| 42     Args: | 51   Args: | 
| 43       pubkey, OpenSSL.crypto.PKey, The public key to verify with. | 52     None | 
| 44     """ | 53   Returns: | 
| 45     self._pubkey = pubkey | 54     None | 
|  | 55   Raises: | 
|  | 56     ImportError if OpenSSL is unavailable. | 
| 46 | 57 | 
| 47   def verify(self, message, signature): | 58   """ | 
| 48     """Verifies a message against a signature. | 59   try: | 
| 49 | 60     _, _package_dir, _ = imp.find_module('OpenSSL') | 
| 50     Args: | 61     if not (os.path.isfile(os.path.join(_package_dir, 'crypto.py')) or | 
| 51       message: string, The message to verify. | 62             os.path.isfile(os.path.join(_package_dir, 'crypto.so')) or | 
| 52       signature: string, The signature on the message. | 63             os.path.isdir(os.path.join(_package_dir, 'crypto'))): | 
| 53 | 64       raise ImportError('No module named OpenSSL.crypto') | 
| 54     Returns: | 65     return | 
| 55       True if message was singed by the private key associated with the public | 66   except ImportError: | 
| 56       key that this object was constructed with. | 67     import OpenSSL.crypto | 
| 57     """ |  | 
| 58     try: |  | 
| 59       crypto.verify(self._pubkey, signature, message, 'sha256') |  | 
| 60       return True |  | 
| 61     except: |  | 
| 62       return False |  | 
| 63 |  | 
| 64   @staticmethod |  | 
| 65   def from_string(key_pem, is_x509_cert): |  | 
| 66     """Construct a Verified instance from a string. |  | 
| 67 |  | 
| 68     Args: |  | 
| 69       key_pem: string, public key in PEM format. |  | 
| 70       is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is |  | 
| 71         expected to be an RSA key in PEM format. |  | 
| 72 |  | 
| 73     Returns: |  | 
| 74       Verifier instance. |  | 
| 75 |  | 
| 76     Raises: |  | 
| 77       OpenSSL.crypto.Error if the key_pem can't be parsed. |  | 
| 78     """ |  | 
| 79     if is_x509_cert: |  | 
| 80       pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem) |  | 
| 81     else: |  | 
| 82       pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem) |  | 
| 83     return Verifier(pubkey) |  | 
| 84 | 68 | 
| 85 | 69 | 
| 86 class Signer(object): | 70 try: | 
| 87   """Signs messages with a private key.""" | 71   _TryOpenSslImport() | 
| 88 | 72   from oauth2client._openssl_crypt import OpenSSLVerifier | 
| 89   def __init__(self, pkey): | 73   from oauth2client._openssl_crypt import OpenSSLSigner | 
| 90     """Constructor. | 74   from oauth2client._openssl_crypt import pkcs12_key_as_pem | 
| 91 | 75 except ImportError: | 
| 92     Args: | 76   OpenSSLVerifier = None | 
| 93       pkey, OpenSSL.crypto.PKey, The private key to sign with. | 77   OpenSSLSigner = None | 
| 94     """ | 78   def pkcs12_key_as_pem(*args, **kwargs): | 
| 95     self._key = pkey | 79     raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.') | 
| 96 |  | 
| 97   def sign(self, message): |  | 
| 98     """Signs a message. |  | 
| 99 |  | 
| 100     Args: |  | 
| 101       message: string, Message to be signed. |  | 
| 102 |  | 
| 103     Returns: |  | 
| 104       string, The signature of the message for the given key. |  | 
| 105     """ |  | 
| 106     return crypto.sign(self._key, message, 'sha256') |  | 
| 107 |  | 
| 108   @staticmethod |  | 
| 109   def from_string(key, password='notasecret'): |  | 
| 110     """Construct a Signer instance from a string. |  | 
| 111 |  | 
| 112     Args: |  | 
| 113       key: string, private key in P12 format. |  | 
| 114       password: string, password for the private key file. |  | 
| 115 |  | 
| 116     Returns: |  | 
| 117       Signer instance. |  | 
| 118 |  | 
| 119     Raises: |  | 
| 120       OpenSSL.crypto.Error if the key can't be parsed. |  | 
| 121     """ |  | 
| 122     pkey = crypto.load_pkcs12(key, password).get_privatekey() |  | 
| 123     return Signer(pkey) |  | 
| 124 | 80 | 
| 125 | 81 | 
| 126 def _urlsafe_b64encode(raw_bytes): | 82 try: | 
| 127   return base64.urlsafe_b64encode(raw_bytes).rstrip('=') | 83   from oauth2client._pycrypto_crypt import PyCryptoVerifier | 
|  | 84   from oauth2client._pycrypto_crypt import PyCryptoSigner | 
|  | 85 except ImportError: | 
|  | 86   PyCryptoVerifier = None | 
|  | 87   PyCryptoSigner = None | 
| 128 | 88 | 
| 129 | 89 | 
| 130 def _urlsafe_b64decode(b64string): | 90 if OpenSSLSigner: | 
| 131   # Guard against unicode strings, which base64 can't handle. | 91   Signer = OpenSSLSigner | 
| 132   b64string = b64string.encode('ascii') | 92   Verifier = OpenSSLVerifier | 
| 133   padded = b64string + '=' * (4 - len(b64string) % 4) | 93 elif PyCryptoSigner: | 
| 134   return base64.urlsafe_b64decode(padded) | 94   Signer = PyCryptoSigner | 
| 135 | 95   Verifier = PyCryptoVerifier | 
| 136 | 96 else: | 
| 137 def _json_encode(data): | 97   raise ImportError('No encryption library found. Please install either ' | 
| 138   return simplejson.dumps(data, separators = (',', ':')) | 98                     'PyOpenSSL, or PyCrypto 2.6 or later') | 
| 139 | 99 | 
| 140 | 100 | 
| 141 def make_signed_jwt(signer, payload): | 101 def make_signed_jwt(signer, payload): | 
| 142   """Make a signed JWT. | 102   """Make a signed JWT. | 
| 143 | 103 | 
| 144   See http://self-issued.info/docs/draft-jones-json-web-token.html. | 104   See http://self-issued.info/docs/draft-jones-json-web-token.html. | 
| 145 | 105 | 
| 146   Args: | 106   Args: | 
| 147     signer: crypt.Signer, Cryptographic signer. | 107     signer: crypt.Signer, Cryptographic signer. | 
| 148     payload: dict, Dictionary of data to convert to JSON and then sign. | 108     payload: dict, Dictionary of data to convert to JSON and then sign. | 
| 149 | 109 | 
| 150   Returns: | 110   Returns: | 
| 151     string, The JWT for the payload. | 111     string, The JWT for the payload. | 
| 152   """ | 112   """ | 
| 153   header = {'typ': 'JWT', 'alg': 'RS256'} | 113   header = {'typ': 'JWT', 'alg': 'RS256'} | 
| 154 | 114 | 
| 155   segments = [ | 115   segments = [ | 
| 156           _urlsafe_b64encode(_json_encode(header)), | 116       _urlsafe_b64encode(_json_encode(header)), | 
| 157           _urlsafe_b64encode(_json_encode(payload)), | 117       _urlsafe_b64encode(_json_encode(payload)), | 
| 158   ] | 118   ] | 
| 159   signing_input = '.'.join(segments) | 119   signing_input = '.'.join(segments) | 
| 160 | 120 | 
| 161   signature = signer.sign(signing_input) | 121   signature = signer.sign(signing_input) | 
| 162   segments.append(_urlsafe_b64encode(signature)) | 122   segments.append(_urlsafe_b64encode(signature)) | 
| 163 | 123 | 
| 164   logging.debug(str(segments)) | 124   logger.debug(str(segments)) | 
| 165 | 125 | 
| 166   return '.'.join(segments) | 126   return '.'.join(segments) | 
| 167 | 127 | 
| 168 | 128 | 
| 169 def verify_signed_jwt_with_certs(jwt, certs, audience): | 129 def verify_signed_jwt_with_certs(jwt, certs, audience): | 
| 170   """Verify a JWT against public certs. | 130   """Verify a JWT against public certs. | 
| 171 | 131 | 
| 172   See http://self-issued.info/docs/draft-jones-json-web-token.html. | 132   See http://self-issued.info/docs/draft-jones-json-web-token.html. | 
| 173 | 133 | 
| 174   Args: | 134   Args: | 
| 175     jwt: string, A JWT. | 135     jwt: string, A JWT. | 
| 176     certs: dict, Dictionary where values of public keys in PEM format. | 136     certs: dict, Dictionary where values of public keys in PEM format. | 
| 177     audience: string, The audience, 'aud', that this JWT should contain. If | 137     audience: string, The audience, 'aud', that this JWT should contain. If | 
| 178       None then the JWT's 'aud' parameter is not verified. | 138       None then the JWT's 'aud' parameter is not verified. | 
| 179 | 139 | 
| 180   Returns: | 140   Returns: | 
| 181     dict, The deserialized JSON payload in the JWT. | 141     dict, The deserialized JSON payload in the JWT. | 
| 182 | 142 | 
| 183   Raises: | 143   Raises: | 
| 184     AppIdentityError if any checks are failed. | 144     AppIdentityError if any checks are failed. | 
| 185   """ | 145   """ | 
| 186   segments = jwt.split('.') | 146   segments = jwt.split('.') | 
| 187 | 147 | 
| 188   if (len(segments) != 3): | 148   if len(segments) != 3: | 
| 189     raise AppIdentityError( | 149     raise AppIdentityError('Wrong number of segments in token: %s' % jwt) | 
| 190       'Wrong number of segments in token: %s' % jwt) |  | 
| 191   signed = '%s.%s' % (segments[0], segments[1]) | 150   signed = '%s.%s' % (segments[0], segments[1]) | 
| 192 | 151 | 
| 193   signature = _urlsafe_b64decode(segments[2]) | 152   signature = _urlsafe_b64decode(segments[2]) | 
| 194 | 153 | 
| 195   # Parse token. | 154   # Parse token. | 
| 196   json_body = _urlsafe_b64decode(segments[1]) | 155   json_body = _urlsafe_b64decode(segments[1]) | 
| 197   try: | 156   try: | 
| 198     parsed = simplejson.loads(json_body) | 157     parsed = json.loads(json_body.decode('utf-8')) | 
| 199   except: | 158   except: | 
| 200     raise AppIdentityError('Can\'t parse token: %s' % json_body) | 159     raise AppIdentityError('Can\'t parse token: %s' % json_body) | 
| 201 | 160 | 
| 202   # Check signature. | 161   # Check signature. | 
| 203   verified = False | 162   verified = False | 
| 204   for (keyname, pem) in certs.items(): | 163   for pem in certs.values(): | 
| 205     verifier = Verifier.from_string(pem, True) | 164     verifier = Verifier.from_string(pem, True) | 
| 206     if (verifier.verify(signed, signature)): | 165     if verifier.verify(signed, signature): | 
| 207       verified = True | 166       verified = True | 
| 208       break | 167       break | 
| 209   if not verified: | 168   if not verified: | 
| 210     raise AppIdentityError('Invalid token signature: %s' % jwt) | 169     raise AppIdentityError('Invalid token signature: %s' % jwt) | 
| 211 | 170 | 
| 212   # Check creation timestamp. | 171   # Check creation timestamp. | 
| 213   iat = parsed.get('iat') | 172   iat = parsed.get('iat') | 
| 214   if iat is None: | 173   if iat is None: | 
| 215     raise AppIdentityError('No iat field in token: %s' % json_body) | 174     raise AppIdentityError('No iat field in token: %s' % json_body) | 
| 216   earliest = iat - CLOCK_SKEW_SECS | 175   earliest = iat - CLOCK_SKEW_SECS | 
| 217 | 176 | 
| 218   # Check expiration timestamp. | 177   # Check expiration timestamp. | 
| 219   now = long(time.time()) | 178   now = int(time.time()) | 
| 220   exp = parsed.get('exp') | 179   exp = parsed.get('exp') | 
| 221   if exp is None: | 180   if exp is None: | 
| 222     raise AppIdentityError('No exp field in token: %s' % json_body) | 181     raise AppIdentityError('No exp field in token: %s' % json_body) | 
| 223   if exp >= now + MAX_TOKEN_LIFETIME_SECS: | 182   if exp >= now + MAX_TOKEN_LIFETIME_SECS: | 
| 224     raise AppIdentityError( | 183     raise AppIdentityError('exp field too far in future: %s' % json_body) | 
| 225       'exp field too far in future: %s' % json_body) |  | 
| 226   latest = exp + CLOCK_SKEW_SECS | 184   latest = exp + CLOCK_SKEW_SECS | 
| 227 | 185 | 
| 228   if now < earliest: | 186   if now < earliest: | 
| 229     raise AppIdentityError('Token used too early, %d < %d: %s' % | 187     raise AppIdentityError('Token used too early, %d < %d: %s' % | 
| 230       (now, earliest, json_body)) | 188                            (now, earliest, json_body)) | 
| 231   if now > latest: | 189   if now > latest: | 
| 232     raise AppIdentityError('Token used too late, %d > %d: %s' % | 190     raise AppIdentityError('Token used too late, %d > %d: %s' % | 
| 233       (now, latest, json_body)) | 191                            (now, latest, json_body)) | 
| 234 | 192 | 
| 235   # Check audience. | 193   # Check audience. | 
| 236   if audience is not None: | 194   if audience is not None: | 
| 237     aud = parsed.get('aud') | 195     aud = parsed.get('aud') | 
| 238     if aud is None: | 196     if aud is None: | 
| 239       raise AppIdentityError('No aud field in token: %s' % json_body) | 197       raise AppIdentityError('No aud field in token: %s' % json_body) | 
| 240     if aud != audience: | 198     if aud != audience: | 
| 241       raise AppIdentityError('Wrong recipient, %s != %s: %s' % | 199       raise AppIdentityError('Wrong recipient, %s != %s: %s' % | 
| 242           (aud, audience, json_body)) | 200                              (aud, audience, json_body)) | 
| 243 | 201 | 
| 244   return parsed | 202   return parsed | 
| OLD | NEW | 
|---|