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