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