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