crypt.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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. import json
  18. import logging
  19. import time
  20. from oauth2client import _helpers
  21. from oauth2client import _pure_python_crypt
  22. RsaSigner = _pure_python_crypt.RsaSigner
  23. RsaVerifier = _pure_python_crypt.RsaVerifier
  24. CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
  25. AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
  26. MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
  27. logger = logging.getLogger(__name__)
  28. class AppIdentityError(Exception):
  29. """Error to indicate crypto failure."""
  30. def _bad_pkcs12_key_as_pem(*args, **kwargs):
  31. raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
  32. try:
  33. from oauth2client import _openssl_crypt
  34. OpenSSLSigner = _openssl_crypt.OpenSSLSigner
  35. OpenSSLVerifier = _openssl_crypt.OpenSSLVerifier
  36. pkcs12_key_as_pem = _openssl_crypt.pkcs12_key_as_pem
  37. except ImportError: # pragma: NO COVER
  38. OpenSSLVerifier = None
  39. OpenSSLSigner = None
  40. pkcs12_key_as_pem = _bad_pkcs12_key_as_pem
  41. try:
  42. from oauth2client import _pycrypto_crypt
  43. PyCryptoSigner = _pycrypto_crypt.PyCryptoSigner
  44. PyCryptoVerifier = _pycrypto_crypt.PyCryptoVerifier
  45. except ImportError: # pragma: NO COVER
  46. PyCryptoVerifier = None
  47. PyCryptoSigner = None
  48. if OpenSSLSigner:
  49. Signer = OpenSSLSigner
  50. Verifier = OpenSSLVerifier
  51. elif PyCryptoSigner: # pragma: NO COVER
  52. Signer = PyCryptoSigner
  53. Verifier = PyCryptoVerifier
  54. else: # pragma: NO COVER
  55. Signer = RsaSigner
  56. Verifier = RsaVerifier
  57. def make_signed_jwt(signer, payload, key_id=None):
  58. """Make a signed JWT.
  59. See http://self-issued.info/docs/draft-jones-json-web-token.html.
  60. Args:
  61. signer: crypt.Signer, Cryptographic signer.
  62. payload: dict, Dictionary of data to convert to JSON and then sign.
  63. key_id: string, (Optional) Key ID header.
  64. Returns:
  65. string, The JWT for the payload.
  66. """
  67. header = {'typ': 'JWT', 'alg': 'RS256'}
  68. if key_id is not None:
  69. header['kid'] = key_id
  70. segments = [
  71. _helpers._urlsafe_b64encode(_helpers._json_encode(header)),
  72. _helpers._urlsafe_b64encode(_helpers._json_encode(payload)),
  73. ]
  74. signing_input = b'.'.join(segments)
  75. signature = signer.sign(signing_input)
  76. segments.append(_helpers._urlsafe_b64encode(signature))
  77. logger.debug(str(segments))
  78. return b'.'.join(segments)
  79. def _verify_signature(message, signature, certs):
  80. """Verifies signed content using a list of certificates.
  81. Args:
  82. message: string or bytes, The message to verify.
  83. signature: string or bytes, The signature on the message.
  84. certs: iterable, certificates in PEM format.
  85. Raises:
  86. AppIdentityError: If none of the certificates can verify the message
  87. against the signature.
  88. """
  89. for pem in certs:
  90. verifier = Verifier.from_string(pem, is_x509_cert=True)
  91. if verifier.verify(message, signature):
  92. return
  93. # If we have not returned, no certificate confirms the signature.
  94. raise AppIdentityError('Invalid token signature')
  95. def _check_audience(payload_dict, audience):
  96. """Checks audience field from a JWT payload.
  97. Does nothing if the passed in ``audience`` is null.
  98. Args:
  99. payload_dict: dict, A dictionary containing a JWT payload.
  100. audience: string or NoneType, an audience to check for in
  101. the JWT payload.
  102. Raises:
  103. AppIdentityError: If there is no ``'aud'`` field in the payload
  104. dictionary but there is an ``audience`` to check.
  105. AppIdentityError: If the ``'aud'`` field in the payload dictionary
  106. does not match the ``audience``.
  107. """
  108. if audience is None:
  109. return
  110. audience_in_payload = payload_dict.get('aud')
  111. if audience_in_payload is None:
  112. raise AppIdentityError(
  113. 'No aud field in token: {0}'.format(payload_dict))
  114. if audience_in_payload != audience:
  115. raise AppIdentityError('Wrong recipient, {0} != {1}: {2}'.format(
  116. audience_in_payload, audience, payload_dict))
  117. def _verify_time_range(payload_dict):
  118. """Verifies the issued at and expiration from a JWT payload.
  119. Makes sure the current time (in UTC) falls between the issued at and
  120. expiration for the JWT (with some skew allowed for via
  121. ``CLOCK_SKEW_SECS``).
  122. Args:
  123. payload_dict: dict, A dictionary containing a JWT payload.
  124. Raises:
  125. AppIdentityError: If there is no ``'iat'`` field in the payload
  126. dictionary.
  127. AppIdentityError: If there is no ``'exp'`` field in the payload
  128. dictionary.
  129. AppIdentityError: If the JWT expiration is too far in the future (i.e.
  130. if the expiration would imply a token lifetime
  131. longer than what is allowed.)
  132. AppIdentityError: If the token appears to have been issued in the
  133. future (up to clock skew).
  134. AppIdentityError: If the token appears to have expired in the past
  135. (up to clock skew).
  136. """
  137. # Get the current time to use throughout.
  138. now = int(time.time())
  139. # Make sure issued at and expiration are in the payload.
  140. issued_at = payload_dict.get('iat')
  141. if issued_at is None:
  142. raise AppIdentityError(
  143. 'No iat field in token: {0}'.format(payload_dict))
  144. expiration = payload_dict.get('exp')
  145. if expiration is None:
  146. raise AppIdentityError(
  147. 'No exp field in token: {0}'.format(payload_dict))
  148. # Make sure the expiration gives an acceptable token lifetime.
  149. if expiration >= now + MAX_TOKEN_LIFETIME_SECS:
  150. raise AppIdentityError(
  151. 'exp field too far in future: {0}'.format(payload_dict))
  152. # Make sure (up to clock skew) that the token wasn't issued in the future.
  153. earliest = issued_at - CLOCK_SKEW_SECS
  154. if now < earliest:
  155. raise AppIdentityError('Token used too early, {0} < {1}: {2}'.format(
  156. now, earliest, payload_dict))
  157. # Make sure (up to clock skew) that the token isn't already expired.
  158. latest = expiration + CLOCK_SKEW_SECS
  159. if now > latest:
  160. raise AppIdentityError('Token used too late, {0} > {1}: {2}'.format(
  161. now, latest, payload_dict))
  162. def verify_signed_jwt_with_certs(jwt, certs, audience=None):
  163. """Verify a JWT against public certs.
  164. See http://self-issued.info/docs/draft-jones-json-web-token.html.
  165. Args:
  166. jwt: string, A JWT.
  167. certs: dict, Dictionary where values of public keys in PEM format.
  168. audience: string, The audience, 'aud', that this JWT should contain. If
  169. None then the JWT's 'aud' parameter is not verified.
  170. Returns:
  171. dict, The deserialized JSON payload in the JWT.
  172. Raises:
  173. AppIdentityError: if any checks are failed.
  174. """
  175. jwt = _helpers._to_bytes(jwt)
  176. if jwt.count(b'.') != 2:
  177. raise AppIdentityError(
  178. 'Wrong number of segments in token: {0}'.format(jwt))
  179. header, payload, signature = jwt.split(b'.')
  180. message_to_sign = header + b'.' + payload
  181. signature = _helpers._urlsafe_b64decode(signature)
  182. # Parse token.
  183. payload_bytes = _helpers._urlsafe_b64decode(payload)
  184. try:
  185. payload_dict = json.loads(_helpers._from_bytes(payload_bytes))
  186. except:
  187. raise AppIdentityError('Can\'t parse token: {0}'.format(payload_bytes))
  188. # Verify that the signature matches the message.
  189. _verify_signature(message_to_sign, signature, certs.values())
  190. # Verify the issued at and created times in the payload.
  191. _verify_time_range(payload_dict)
  192. # Check audience.
  193. _check_audience(payload_dict, audience)
  194. return payload_dict