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 18import base64 19import imp 20import json 21import logging 22import os 23import sys 24import time 25 26import six 27 28 29CLOCK_SKEW_SECS = 300 # 5 minutes in seconds 30AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds 31MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds 32 33 34logger = logging.getLogger(__name__) 35 36 37class AppIdentityError(Exception): 38 pass 39 40 41def _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 66try: 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()) 191except 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 198try: 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 309except ImportError: 310 PyCryptoVerifier = None 311 PyCryptoSigner = None 312 313 314if OpenSSLSigner: 315 Signer = OpenSSLSigner 316 Verifier = OpenSSLVerifier 317elif PyCryptoSigner: 318 Signer = PyCryptoSigner 319 Verifier = PyCryptoVerifier 320else: 321 raise ImportError('No encryption library found. Please install either ' 322 'PyOpenSSL, or PyCrypto 2.6 or later') 323 324 325def _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 342def _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 348def _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 356def _json_encode(data): 357 return json.dumps(data, separators=(',', ':')) 358 359 360def 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 388def 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