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

Source Code for Module oauth2client.crypt

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