34
34
35
35
36
36
class AppIdentityError (Exception ):
37
- pass
37
+ """Error to indicate crypto failure."""
38
+
39
+
40
+ def _bad_pkcs12_key_as_pem (* args , ** kwargs ):
41
+ raise NotImplementedError ('pkcs12_key_as_pem requires OpenSSL.' )
38
42
39
43
40
44
try :
41
45
from oauth2client ._openssl_crypt import OpenSSLVerifier
42
46
from oauth2client ._openssl_crypt import OpenSSLSigner
43
47
from oauth2client ._openssl_crypt import pkcs12_key_as_pem
44
- except ImportError :
48
+ except ImportError : # pragma: NO COVER
45
49
OpenSSLVerifier = None
46
50
OpenSSLSigner = None
47
-
48
- def pkcs12_key_as_pem (* args , ** kwargs ):
49
- raise NotImplementedError ('pkcs12_key_as_pem requires OpenSSL.' )
51
+ pkcs12_key_as_pem = _bad_pkcs12_key_as_pem
50
52
51
53
try :
52
54
from oauth2client ._pycrypto_crypt import PyCryptoVerifier
53
55
from oauth2client ._pycrypto_crypt import PyCryptoSigner
54
- except ImportError :
56
+ except ImportError : # pragma: NO COVER
55
57
PyCryptoVerifier = None
56
58
PyCryptoSigner = None
57
59
58
60
59
61
if OpenSSLSigner :
60
62
Signer = OpenSSLSigner
61
63
Verifier = OpenSSLVerifier
62
- elif PyCryptoSigner :
64
+ elif PyCryptoSigner : # pragma: NO COVER
63
65
Signer = PyCryptoSigner
64
66
Verifier = PyCryptoVerifier
65
- else :
67
+ else : # pragma: NO COVER
66
68
raise ImportError ('No encryption library found. Please install either '
67
69
'PyOpenSSL, or PyCrypto 2.6 or later' )
68
70
@@ -95,7 +97,107 @@ def make_signed_jwt(signer, payload):
95
97
return b'.' .join (segments )
96
98
97
99
98
- def verify_signed_jwt_with_certs (jwt , certs , audience ):
100
+ def _verify_signature (message , signature , certs ):
101
+ """Verifies signed content using a list of certificates.
102
+
103
+ Args:
104
+ message: string or bytes, The message to verify.
105
+ signature: string or bytes, The signature on the message.
106
+ certs: iterable, certificates in PEM format.
107
+
108
+ Raises:
109
+ AppIdentityError: If none of the certificates can verify the message
110
+ against the signature.
111
+ """
112
+ for pem in certs :
113
+ verifier = Verifier .from_string (pem , is_x509_cert = True )
114
+ if verifier .verify (message , signature ):
115
+ return
116
+
117
+ # If we have not returned, no certificate confirms the signature.
118
+ raise AppIdentityError ('Invalid token signature' )
119
+
120
+
121
+ def _check_audience (payload_dict , audience ):
122
+ """Checks audience field from a JWT payload.
123
+
124
+ Does nothing if the passed in ``audience`` is null.
125
+
126
+ Args:
127
+ payload_dict: dict, A dictionary containing a JWT payload.
128
+ audience: string or NoneType, an audience to check for in
129
+ the JWT payload.
130
+
131
+ Raises:
132
+ AppIdentityError: If there is no ``'aud'`` field in the payload
133
+ dictionary but there is an ``audience`` to check.
134
+ AppIdentityError: If the ``'aud'`` field in the payload dictionary
135
+ does not match the ``audience``.
136
+ """
137
+ if audience is None :
138
+ return
139
+
140
+ audience_in_payload = payload_dict .get ('aud' )
141
+ if audience_in_payload is None :
142
+ raise AppIdentityError ('No aud field in token: %s' %
143
+ (payload_dict ,))
144
+ if audience_in_payload != audience :
145
+ raise AppIdentityError ('Wrong recipient, %s != %s: %s' %
146
+ (audience_in_payload , audience , payload_dict ))
147
+
148
+
149
+ def _verify_time_range (payload_dict ):
150
+ """Verifies the issued at and expiration from a JWT payload.
151
+
152
+ Makes sure the current time (in UTC) falls between the issued at and
153
+ expiration for the JWT (with some skew allowed for via
154
+ ``CLOCK_SKEW_SECS``).
155
+
156
+ Args:
157
+ payload_dict: dict, A dictionary containing a JWT payload.
158
+
159
+ Raises:
160
+ AppIdentityError: If there is no ``'iat'`` field in the payload
161
+ dictionary.
162
+ AppIdentityError: If there is no ``'exp'`` field in the payload
163
+ dictionary.
164
+ AppIdentityError: If the JWT expiration is too far in the future (i.e.
165
+ if the expiration would imply a token lifetime
166
+ longer than what is allowed.)
167
+ AppIdentityError: If the token appears to have been issued in the
168
+ future (up to clock skew).
169
+ AppIdentityError: If the token appears to have expired in the past
170
+ (up to clock skew).
171
+ """
172
+ # Get the current time to use throughout.
173
+ now = int (time .time ())
174
+
175
+ # Make sure issued at and expiration are in the payload.
176
+ issued_at = payload_dict .get ('iat' )
177
+ if issued_at is None :
178
+ raise AppIdentityError ('No iat field in token: %s' % (payload_dict ,))
179
+ expiration = payload_dict .get ('exp' )
180
+ if expiration is None :
181
+ raise AppIdentityError ('No exp field in token: %s' % (payload_dict ,))
182
+
183
+ # Make sure the expiration gives an acceptable token lifetime.
184
+ if expiration >= now + MAX_TOKEN_LIFETIME_SECS :
185
+ raise AppIdentityError ('exp field too far in future: %s' %
186
+ (payload_dict ,))
187
+
188
+ # Make sure (up to clock skew) that the token wasn't issued in the future.
189
+ earliest = issued_at - CLOCK_SKEW_SECS
190
+ if now < earliest :
191
+ raise AppIdentityError ('Token used too early, %d < %d: %s' %
192
+ (now , earliest , payload_dict ))
193
+ # Make sure (up to clock skew) that the token isn't already expired.
194
+ latest = expiration + CLOCK_SKEW_SECS
195
+ if now > latest :
196
+ raise AppIdentityError ('Token used too late, %d > %d: %s' %
197
+ (now , latest , payload_dict ))
198
+
199
+
200
+ def verify_signed_jwt_with_certs (jwt , certs , audience = None ):
99
201
"""Verify a JWT against public certs.
100
202
101
203
See http://self-issued.info/docs/draft-jones-json-web-token.html.
@@ -110,63 +212,32 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
110
212
dict, The deserialized JSON payload in the JWT.
111
213
112
214
Raises:
113
- AppIdentityError if any checks are failed.
215
+ AppIdentityError: if any checks are failed.
114
216
"""
115
217
jwt = _to_bytes (jwt )
116
- segments = jwt .split (b'.' )
117
218
118
- if len ( segments ) != 3 :
119
- raise AppIdentityError ('Wrong number of segments in token: %s' % jwt )
120
- signed = segments [ 0 ] + b'.' + segments [ 1 ]
219
+ if jwt . count ( b'.' ) != 2 :
220
+ raise AppIdentityError (
221
+ 'Wrong number of segments in token: %s' % ( jwt ,))
121
222
122
- signature = _urlsafe_b64decode (segments [2 ])
223
+ header , payload , signature = jwt .split (b'.' )
224
+ message_to_sign = header + b'.' + payload
225
+ signature = _urlsafe_b64decode (signature )
123
226
124
227
# Parse token.
125
- json_body = _urlsafe_b64decode (segments [ 1 ] )
228
+ payload_bytes = _urlsafe_b64decode (payload )
126
229
try :
127
- parsed = json .loads (_from_bytes (json_body ))
230
+ payload_dict = json .loads (_from_bytes (payload_bytes ))
128
231
except :
129
- raise AppIdentityError ('Can\' t parse token: %s' % json_body )
130
-
131
- # Check signature.
132
- verified = False
133
- for pem in certs .values ():
134
- verifier = Verifier .from_string (pem , True )
135
- if verifier .verify (signed , signature ):
136
- verified = True
137
- break
138
- if not verified :
139
- raise AppIdentityError ('Invalid token signature: %s' % jwt )
140
-
141
- # Check creation timestamp.
142
- iat = parsed .get ('iat' )
143
- if iat is None :
144
- raise AppIdentityError ('No iat field in token: %s' % json_body )
145
- earliest = iat - CLOCK_SKEW_SECS
146
-
147
- # Check expiration timestamp.
148
- now = int (time .time ())
149
- exp = parsed .get ('exp' )
150
- if exp is None :
151
- raise AppIdentityError ('No exp field in token: %s' % json_body )
152
- if exp >= now + MAX_TOKEN_LIFETIME_SECS :
153
- raise AppIdentityError ('exp field too far in future: %s' % json_body )
154
- latest = exp + CLOCK_SKEW_SECS
232
+ raise AppIdentityError ('Can\' t parse token: %s' % (payload_bytes ,))
155
233
156
- if now < earliest :
157
- raise AppIdentityError ('Token used too early, %d < %d: %s' %
158
- (now , earliest , json_body ))
159
- if now > latest :
160
- raise AppIdentityError ('Token used too late, %d > %d: %s' %
161
- (now , latest , json_body ))
234
+ # Verify that the signature matches the message.
235
+ _verify_signature (message_to_sign , signature , certs .values ())
236
+
237
+ # Verify the issued at and created times in the payload.
238
+ _verify_time_range (payload_dict )
162
239
163
240
# Check audience.
164
- if audience is not None :
165
- aud = parsed .get ('aud' )
166
- if aud is None :
167
- raise AppIdentityError ('No aud field in token: %s' % json_body )
168
- if aud != audience :
169
- raise AppIdentityError ('Wrong recipient, %s != %s: %s' %
170
- (aud , audience , json_body ))
171
-
172
- return parsed
241
+ _check_audience (payload_dict , audience )
242
+
243
+ return payload_dict
0 commit comments