2
# -*- coding: utf-8 -*-
4
# Copyright (C) 2011 Google Inc.
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
10
# http://www.apache.org/licenses/LICENSE-2.0
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.
23
from OpenSSL import crypto
24
from anyjson import simplejson
27
logger = logging.getLogger(__name__)
29
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
30
AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
31
MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
34
class AppIdentityError(Exception):
38
class Verifier(object):
39
"""Verifies the signature on a message."""
41
def __init__(self, pubkey):
45
pubkey, OpenSSL.crypto.PKey, The public key to verify with.
49
def verify(self, message, signature):
50
"""Verifies a message against a signature.
53
message: string, The message to verify.
54
signature: string, The signature on the message.
57
True if message was singed by the private key associated with the public
58
key that this object was constructed with.
61
crypto.verify(self._pubkey, signature, message, 'sha256')
67
def from_string(key_pem, is_x509_cert):
68
"""Construct a Verified instance from a string.
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.
79
OpenSSL.crypto.Error if the key_pem can't be parsed.
82
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
84
pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
85
return Verifier(pubkey)
89
"""Signs messages with a private key."""
91
def __init__(self, pkey):
95
pkey, OpenSSL.crypto.PKey, The private key to sign with.
99
def sign(self, message):
103
message: string, Message to be signed.
106
string, The signature of the message for the given key.
108
return crypto.sign(self._key, message, 'sha256')
111
def from_string(key, password='notasecret'):
112
"""Construct a Signer instance from a string.
115
key: string, private key in P12 format.
116
password: string, password for the private key file.
122
OpenSSL.crypto.Error if the key can't be parsed.
124
pkey = crypto.load_pkcs12(key, password).get_privatekey()
128
def _urlsafe_b64encode(raw_bytes):
129
return base64.urlsafe_b64encode(raw_bytes).rstrip('=')
132
def _urlsafe_b64decode(b64string):
133
# Guard against unicode strings, which base64 can't handle.
134
b64string = b64string.encode('ascii')
135
padded = b64string + '=' * (4 - len(b64string) % 4)
136
return base64.urlsafe_b64decode(padded)
139
def _json_encode(data):
140
return simplejson.dumps(data, separators = (',', ':'))
143
def make_signed_jwt(signer, payload):
144
"""Make a signed JWT.
146
See http://self-issued.info/docs/draft-jones-json-web-token.html.
149
signer: crypt.Signer, Cryptographic signer.
150
payload: dict, Dictionary of data to convert to JSON and then sign.
153
string, The JWT for the payload.
155
header = {'typ': 'JWT', 'alg': 'RS256'}
158
_urlsafe_b64encode(_json_encode(header)),
159
_urlsafe_b64encode(_json_encode(payload)),
161
signing_input = '.'.join(segments)
163
signature = signer.sign(signing_input)
164
segments.append(_urlsafe_b64encode(signature))
166
logger.debug(str(segments))
168
return '.'.join(segments)
171
def verify_signed_jwt_with_certs(jwt, certs, audience):
172
"""Verify a JWT against public certs.
174
See http://self-issued.info/docs/draft-jones-json-web-token.html.
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.
183
dict, The deserialized JSON payload in the JWT.
186
AppIdentityError if any checks are failed.
188
segments = jwt.split('.')
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])
195
signature = _urlsafe_b64decode(segments[2])
198
json_body = _urlsafe_b64decode(segments[1])
200
parsed = simplejson.loads(json_body)
202
raise AppIdentityError('Can\'t parse token: %s' % json_body)
206
for (keyname, pem) in certs.items():
207
verifier = Verifier.from_string(pem, True)
208
if (verifier.verify(signed, signature)):
212
raise AppIdentityError('Invalid token signature: %s' % jwt)
214
# Check creation timestamp.
215
iat = parsed.get('iat')
217
raise AppIdentityError('No iat field in token: %s' % json_body)
218
earliest = iat - CLOCK_SKEW_SECS
220
# Check expiration timestamp.
221
now = long(time.time())
222
exp = parsed.get('exp')
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
231
raise AppIdentityError('Token used too early, %d < %d: %s' %
232
(now, earliest, json_body))
234
raise AppIdentityError('Token used too late, %d > %d: %s' %
235
(now, latest, json_body))
238
if audience is not None:
239
aud = parsed.get('aud')
241
raise AppIdentityError('No aud field in token: %s' % json_body)
243
raise AppIdentityError('Wrong recipient, %s != %s: %s' %
244
(aud, audience, json_body))