blob: d2d7a3bbdf4c4f00402868f74bd7bf8e6a72eb3c [file] [log] [blame]
Joe Gregorio8b4c1732011-12-06 11:28:29 -05001#!/usr/bin/python2.4
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2011 Google Inc.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import base64
19import hashlib
20import logging
21import time
22
Joe Gregorio549230c2012-01-11 10:38:05 -050023from anyjson import simplejson
Joe Gregorio8b4c1732011-12-06 11:28:29 -050024
25
26CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
27AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
28MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
29
30
Joe Gregorio0b723c22013-01-03 15:00:50 -050031logger = logging.getLogger(__name__)
32
33
Joe Gregorio8b4c1732011-12-06 11:28:29 -050034class AppIdentityError(Exception):
35 pass
36
37
Joe Gregorio0b723c22013-01-03 15:00:50 -050038try:
39 from OpenSSL import crypto
Joe Gregorio8b4c1732011-12-06 11:28:29 -050040
Joe Gregorio0b723c22013-01-03 15:00:50 -050041 class OpenSSLVerifier(object):
42 """Verifies the signature on a message."""
Joe Gregorio8b4c1732011-12-06 11:28:29 -050043
Joe Gregorio0b723c22013-01-03 15:00:50 -050044 def __init__(self, pubkey):
45 """Constructor.
Joe Gregorio8b4c1732011-12-06 11:28:29 -050046
Joe Gregorio0b723c22013-01-03 15:00:50 -050047 Args:
48 pubkey, OpenSSL.crypto.PKey, The public key to verify with.
49 """
50 self._pubkey = pubkey
Joe Gregorio8b4c1732011-12-06 11:28:29 -050051
Joe Gregorio0b723c22013-01-03 15:00:50 -050052 def verify(self, message, signature):
53 """Verifies a message against a signature.
Joe Gregorio8b4c1732011-12-06 11:28:29 -050054
Joe Gregorio0b723c22013-01-03 15:00:50 -050055 Args:
56 message: string, The message to verify.
57 signature: string, The signature on the message.
Joe Gregorio8b4c1732011-12-06 11:28:29 -050058
Joe Gregorio0b723c22013-01-03 15:00:50 -050059 Returns:
60 True if message was signed by the private key associated with the public
61 key that this object was constructed with.
62 """
63 try:
64 crypto.verify(self._pubkey, signature, message, 'sha256')
65 return True
66 except:
67 return False
Joe Gregorio8b4c1732011-12-06 11:28:29 -050068
Joe Gregorio0b723c22013-01-03 15:00:50 -050069 @staticmethod
70 def from_string(key_pem, is_x509_cert):
71 """Construct a Verified instance from a string.
Joe Gregorio8b4c1732011-12-06 11:28:29 -050072
Joe Gregorio0b723c22013-01-03 15:00:50 -050073 Args:
74 key_pem: string, public key in PEM format.
75 is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
76 expected to be an RSA key in PEM format.
Joe Gregorio8b4c1732011-12-06 11:28:29 -050077
Joe Gregorio0b723c22013-01-03 15:00:50 -050078 Returns:
79 Verifier instance.
Joe Gregorio8b4c1732011-12-06 11:28:29 -050080
Joe Gregorio0b723c22013-01-03 15:00:50 -050081 Raises:
82 OpenSSL.crypto.Error if the key_pem can't be parsed.
83 """
84 if is_x509_cert:
85 pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
86 else:
87 pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
88 return OpenSSLVerifier(pubkey)
89
90
91 class OpenSSLSigner(object):
92 """Signs messages with a private key."""
93
94 def __init__(self, pkey):
95 """Constructor.
96
97 Args:
98 pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
99 """
100 self._key = pkey
101
102 def sign(self, message):
103 """Signs a message.
104
105 Args:
106 message: string, Message to be signed.
107
108 Returns:
109 string, The signature of the message for the given key.
110 """
111 return crypto.sign(self._key, message, 'sha256')
112
113 @staticmethod
114 def from_string(key, password='notasecret'):
115 """Construct a Signer instance from a string.
116
117 Args:
118 key: string, private key in PKCS12 or PEM format.
119 password: string, password for the private key file.
120
121 Returns:
122 Signer instance.
123
124 Raises:
125 OpenSSL.crypto.Error if the key can't be parsed.
126 """
Joe Gregorio89d832a2013-10-29 16:02:32 -0400127 parsed_pem_key = _parse_pem_key(key)
128 if parsed_pem_key:
129 pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
Joe Gregorio0b723c22013-01-03 15:00:50 -0500130 else:
131 pkey = crypto.load_pkcs12(key, password).get_privatekey()
132 return OpenSSLSigner(pkey)
133
134except ImportError:
135 OpenSSLVerifier = None
136 OpenSSLSigner = None
137
138
139try:
140 from Crypto.PublicKey import RSA
141 from Crypto.Hash import SHA256
142 from Crypto.Signature import PKCS1_v1_5
143
144
145 class PyCryptoVerifier(object):
146 """Verifies the signature on a message."""
147
148 def __init__(self, pubkey):
149 """Constructor.
150
151 Args:
152 pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with.
153 """
154 self._pubkey = pubkey
155
156 def verify(self, message, signature):
157 """Verifies a message against a signature.
158
159 Args:
160 message: string, The message to verify.
161 signature: string, The signature on the message.
162
163 Returns:
164 True if message was signed by the private key associated with the public
165 key that this object was constructed with.
166 """
167 try:
168 return PKCS1_v1_5.new(self._pubkey).verify(
169 SHA256.new(message), signature)
170 except:
171 return False
172
173 @staticmethod
174 def from_string(key_pem, is_x509_cert):
175 """Construct a Verified instance from a string.
176
177 Args:
178 key_pem: string, public key in PEM format.
179 is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
180 expected to be an RSA key in PEM format.
181
182 Returns:
183 Verifier instance.
184
185 Raises:
186 NotImplementedError if is_x509_cert is true.
187 """
188 if is_x509_cert:
189 raise NotImplementedError(
190 'X509 certs are not supported by the PyCrypto library. '
191 'Try using PyOpenSSL if native code is an option.')
192 else:
193 pubkey = RSA.importKey(key_pem)
194 return PyCryptoVerifier(pubkey)
195
196
197 class PyCryptoSigner(object):
198 """Signs messages with a private key."""
199
200 def __init__(self, pkey):
201 """Constructor.
202
203 Args:
204 pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
205 """
206 self._key = pkey
207
208 def sign(self, message):
209 """Signs a message.
210
211 Args:
212 message: string, Message to be signed.
213
214 Returns:
215 string, The signature of the message for the given key.
216 """
217 return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
218
219 @staticmethod
220 def from_string(key, password='notasecret'):
221 """Construct a Signer instance from a string.
222
223 Args:
224 key: string, private key in PEM format.
225 password: string, password for private key file. Unused for PEM files.
226
227 Returns:
228 Signer instance.
229
230 Raises:
231 NotImplementedError if they key isn't in PEM format.
232 """
Joe Gregorio89d832a2013-10-29 16:02:32 -0400233 parsed_pem_key = _parse_pem_key(key)
234 if parsed_pem_key:
235 pkey = RSA.importKey(parsed_pem_key)
Joe Gregorio0b723c22013-01-03 15:00:50 -0500236 else:
237 raise NotImplementedError(
238 'PKCS12 format is not supported by the PyCrpto library. '
239 'Try converting to a "PEM" '
240 '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) '
241 'or using PyOpenSSL if native code is an option.')
242 return PyCryptoSigner(pkey)
243
244except ImportError:
245 PyCryptoVerifier = None
246 PyCryptoSigner = None
247
248
249if OpenSSLSigner:
250 Signer = OpenSSLSigner
251 Verifier = OpenSSLVerifier
252elif PyCryptoSigner:
253 Signer = PyCryptoSigner
254 Verifier = PyCryptoVerifier
255else:
256 raise ImportError('No encryption library found. Please install either '
257 'PyOpenSSL, or PyCrypto 2.6 or later')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500258
259
Joe Gregorio89d832a2013-10-29 16:02:32 -0400260def _parse_pem_key(raw_key_input):
261 """Identify and extract PEM keys.
262
263 Determines whether the given key is in the format of PEM key, and extracts
264 the relevant part of the key if it is.
265
266 Args:
267 raw_key_input: The contents of a private key file (either PEM or PKCS12).
268
269 Returns:
270 string, The actual key if the contents are from a PEM file, or else None.
271 """
272 offset = raw_key_input.find('-----BEGIN ')
273 if offset != -1:
274 return raw_key_input[offset:]
275 else:
276 return None
277
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500278def _urlsafe_b64encode(raw_bytes):
279 return base64.urlsafe_b64encode(raw_bytes).rstrip('=')
280
281
282def _urlsafe_b64decode(b64string):
Joe Gregoriobd512b52011-12-06 15:39:26 -0500283 # Guard against unicode strings, which base64 can't handle.
284 b64string = b64string.encode('ascii')
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500285 padded = b64string + '=' * (4 - len(b64string) % 4)
286 return base64.urlsafe_b64decode(padded)
287
288
289def _json_encode(data):
290 return simplejson.dumps(data, separators = (',', ':'))
291
292
293def make_signed_jwt(signer, payload):
294 """Make a signed JWT.
295
296 See http://self-issued.info/docs/draft-jones-json-web-token.html.
297
298 Args:
299 signer: crypt.Signer, Cryptographic signer.
300 payload: dict, Dictionary of data to convert to JSON and then sign.
301
302 Returns:
303 string, The JWT for the payload.
304 """
305 header = {'typ': 'JWT', 'alg': 'RS256'}
306
307 segments = [
308 _urlsafe_b64encode(_json_encode(header)),
309 _urlsafe_b64encode(_json_encode(payload)),
310 ]
311 signing_input = '.'.join(segments)
312
313 signature = signer.sign(signing_input)
314 segments.append(_urlsafe_b64encode(signature))
315
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400316 logger.debug(str(segments))
Joe Gregorio8b4c1732011-12-06 11:28:29 -0500317
318 return '.'.join(segments)
319
320
321def verify_signed_jwt_with_certs(jwt, certs, audience):
322 """Verify a JWT against public certs.
323
324 See http://self-issued.info/docs/draft-jones-json-web-token.html.
325
326 Args:
327 jwt: string, A JWT.
328 certs: dict, Dictionary where values of public keys in PEM format.
329 audience: string, The audience, 'aud', that this JWT should contain. If
330 None then the JWT's 'aud' parameter is not verified.
331
332 Returns:
333 dict, The deserialized JSON payload in the JWT.
334
335 Raises:
336 AppIdentityError if any checks are failed.
337 """
338 segments = jwt.split('.')
339
340 if (len(segments) != 3):
341 raise AppIdentityError(
342 'Wrong number of segments in token: %s' % jwt)
343 signed = '%s.%s' % (segments[0], segments[1])
344
345 signature = _urlsafe_b64decode(segments[2])
346
347 # Parse token.
348 json_body = _urlsafe_b64decode(segments[1])
349 try:
350 parsed = simplejson.loads(json_body)
351 except:
352 raise AppIdentityError('Can\'t parse token: %s' % json_body)
353
354 # Check signature.
355 verified = False
356 for (keyname, pem) in certs.items():
357 verifier = Verifier.from_string(pem, True)
358 if (verifier.verify(signed, signature)):
359 verified = True
360 break
361 if not verified:
362 raise AppIdentityError('Invalid token signature: %s' % jwt)
363
364 # Check creation timestamp.
365 iat = parsed.get('iat')
366 if iat is None:
367 raise AppIdentityError('No iat field in token: %s' % json_body)
368 earliest = iat - CLOCK_SKEW_SECS
369
370 # Check expiration timestamp.
371 now = long(time.time())
372 exp = parsed.get('exp')
373 if exp is None:
374 raise AppIdentityError('No exp field in token: %s' % json_body)
375 if exp >= now + MAX_TOKEN_LIFETIME_SECS:
376 raise AppIdentityError(
377 'exp field too far in future: %s' % json_body)
378 latest = exp + CLOCK_SKEW_SECS
379
380 if now < earliest:
381 raise AppIdentityError('Token used too early, %d < %d: %s' %
382 (now, earliest, json_body))
383 if now > latest:
384 raise AppIdentityError('Token used too late, %d > %d: %s' %
385 (now, latest, json_body))
386
387 # Check audience.
388 if audience is not None:
389 aud = parsed.get('aud')
390 if aud is None:
391 raise AppIdentityError('No aud field in token: %s' % json_body)
392 if aud != audience:
393 raise AppIdentityError('Wrong recipient, %s != %s: %s' %
394 (aud, audience, json_body))
395
396 return parsed