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