Package oauth2client :: Module crypt
[hide private]
[frames] | no frames]

Source Code for Module oauth2client.crypt

  1  #!/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   
 18  import base64 
 19  import hashlib 
 20  import logging 
 21  import time 
 22   
 23  from OpenSSL import crypto 
 24  from anyjson import simplejson 
 25   
 26   
 27  logger = logging.getLogger(__name__) 
 28   
 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 
32 33 34 -class AppIdentityError(Exception):
35 pass
36
37 38 -class 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 88 -class 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 128 -def _urlsafe_b64encode(raw_bytes):
129 return base64.urlsafe_b64encode(raw_bytes).rstrip('=')
130
131 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)
137
138 139 -def _json_encode(data):
140 return simplejson.dumps(data, separators = (',', ':'))
141
142 143 -def 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 166 logger.debug(str(segments)) 167 168 return '.'.join(segments)
169
170 171 -def 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
247