blob: 523c921df84bf5b11cf0be87a04eabb976b08d57 [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
23from OpenSSL import crypto
24
25try: # pragma: no cover
26 import simplejson
27except ImportError: # pragma: no cover
28 try:
29 # Try to import from django, should work on App Engine
30 from django.utils import simplejson
31 except ImportError:
32 # Should work for Python2.6 and higher.
33 import json as simplejson
34
35
36CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
37AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
38MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
39
40
41class AppIdentityError(Exception):
42 pass
43
44
45class Verifier(object):
46 """Verifies the signature on a message."""
47
48 def __init__(self, pubkey):
49 """Constructor.
50
51 Args:
52 pubkey, OpenSSL.crypto.PKey, The public key to verify with.
53 """
54 self._pubkey = pubkey
55
56 def verify(self, message, signature):
57 """Verifies a message against a signature.
58
59 Args:
60 message: string, The message to verify.
61 signature: string, The signature on the message.
62
63 Returns:
64 True if message was singed by the private key associated with the public
65 key that this object was constructed with.
66 """
67 try:
68 crypto.verify(self._pubkey, signature, message, 'sha256')
69 return True
70 except:
71 return False
72
73 @staticmethod
74 def from_string(key_pem, is_x509_cert):
75 """Construct a Verified instance from a string.
76
77 Args:
78 key_pem: string, public key in PEM format.
79 is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
80 expected to be an RSA key in PEM format.
81
82 Returns:
83 Verifier instance.
84
85 Raises:
86 OpenSSL.crypto.Error if the key_pem can't be parsed.
87 """
88 if is_x509_cert:
89 pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
90 else:
91 pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
92 return Verifier(pubkey)
93
94
95class Signer(object):
96 """Signs messages with a private key."""
97
98 def __init__(self, pkey):
99 """Constructor.
100
101 Args:
102 pkey, OpenSSL.crypto.PKey, The private key to sign with.
103 """
104 self._key = pkey
105
106 def sign(self, message):
107 """Signs a message.
108
109 Args:
110 message: string, Message to be signed.
111
112 Returns:
113 string, The signature of the message for the given key.
114 """
115 return crypto.sign(self._key, message, 'sha256')
116
117 @staticmethod
118 def from_string(key, password='notasecret'):
119 """Construct a Signer instance from a string.
120
121 Args:
122 key: string, private key in P12 format.
123 password: string, password for the private key file.
124
125 Returns:
126 Signer instance.
127
128 Raises:
129 OpenSSL.crypto.Error if the key can't be parsed.
130 """
131 pkey = crypto.load_pkcs12(key, password).get_privatekey()
132 return Signer(pkey)
133
134
135def _urlsafe_b64encode(raw_bytes):
136 return base64.urlsafe_b64encode(raw_bytes).rstrip('=')
137
138
139def _urlsafe_b64decode(b64string):
140 padded = b64string + '=' * (4 - len(b64string) % 4)
141 return base64.urlsafe_b64decode(padded)
142
143
144def _json_encode(data):
145 return simplejson.dumps(data, separators = (',', ':'))
146
147
148def make_signed_jwt(signer, payload):
149 """Make a signed JWT.
150
151 See http://self-issued.info/docs/draft-jones-json-web-token.html.
152
153 Args:
154 signer: crypt.Signer, Cryptographic signer.
155 payload: dict, Dictionary of data to convert to JSON and then sign.
156
157 Returns:
158 string, The JWT for the payload.
159 """
160 header = {'typ': 'JWT', 'alg': 'RS256'}
161
162 segments = [
163 _urlsafe_b64encode(_json_encode(header)),
164 _urlsafe_b64encode(_json_encode(payload)),
165 ]
166 signing_input = '.'.join(segments)
167
168 signature = signer.sign(signing_input)
169 segments.append(_urlsafe_b64encode(signature))
170
171 logging.debug(str(segments))
172
173 return '.'.join(segments)
174
175
176def verify_signed_jwt_with_certs(jwt, certs, audience):
177 """Verify a JWT against public certs.
178
179 See http://self-issued.info/docs/draft-jones-json-web-token.html.
180
181 Args:
182 jwt: string, A JWT.
183 certs: dict, Dictionary where values of public keys in PEM format.
184 audience: string, The audience, 'aud', that this JWT should contain. If
185 None then the JWT's 'aud' parameter is not verified.
186
187 Returns:
188 dict, The deserialized JSON payload in the JWT.
189
190 Raises:
191 AppIdentityError if any checks are failed.
192 """
193 segments = jwt.split('.')
194
195 if (len(segments) != 3):
196 raise AppIdentityError(
197 'Wrong number of segments in token: %s' % jwt)
198 signed = '%s.%s' % (segments[0], segments[1])
199
200 signature = _urlsafe_b64decode(segments[2])
201
202 # Parse token.
203 json_body = _urlsafe_b64decode(segments[1])
204 try:
205 parsed = simplejson.loads(json_body)
206 except:
207 raise AppIdentityError('Can\'t parse token: %s' % json_body)
208
209 # Check signature.
210 verified = False
211 for (keyname, pem) in certs.items():
212 verifier = Verifier.from_string(pem, True)
213 if (verifier.verify(signed, signature)):
214 verified = True
215 break
216 if not verified:
217 raise AppIdentityError('Invalid token signature: %s' % jwt)
218
219 # Check creation timestamp.
220 iat = parsed.get('iat')
221 if iat is None:
222 raise AppIdentityError('No iat field in token: %s' % json_body)
223 earliest = iat - CLOCK_SKEW_SECS
224
225 # Check expiration timestamp.
226 now = long(time.time())
227 exp = parsed.get('exp')
228 if exp is None:
229 raise AppIdentityError('No exp field in token: %s' % json_body)
230 if exp >= now + MAX_TOKEN_LIFETIME_SECS:
231 raise AppIdentityError(
232 'exp field too far in future: %s' % json_body)
233 latest = exp + CLOCK_SKEW_SECS
234
235 if now < earliest:
236 raise AppIdentityError('Token used too early, %d < %d: %s' %
237 (now, earliest, json_body))
238 if now > latest:
239 raise AppIdentityError('Token used too late, %d > %d: %s' %
240 (now, latest, json_body))
241
242 # Check audience.
243 if audience is not None:
244 aud = parsed.get('aud')
245 if aud is None:
246 raise AppIdentityError('No aud field in token: %s' % json_body)
247 if aud != audience:
248 raise AppIdentityError('Wrong recipient, %s != %s: %s' %
249 (aud, audience, json_body))
250
251 return parsed