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