1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 """Crypto-related routines for oauth2client."""
17
18 import base64
19 import json
20 import logging
21 import sys
22 import time
23
24 import six
25
26
27 CLOCK_SKEW_SECS = 300
28 AUTH_TOKEN_LIFETIME_SECS = 300
29 MAX_TOKEN_LIFETIME_SECS = 86400
30
31
32 logger = logging.getLogger(__name__)
37
38
39 try:
40 from OpenSSL import crypto
43 """Verifies the signature on a message."""
44
46 """Constructor.
47
48 Args:
49 pubkey, OpenSSL.crypto.PKey, The public key to verify with.
50 """
51 self._pubkey = pubkey
52
53 - def verify(self, message, signature):
54 """Verifies a message against a signature.
55
56 Args:
57 message: string, The message to verify.
58 signature: string, The signature on the message.
59
60 Returns:
61 True if message was signed by the private key associated with the public
62 key that this object was constructed with.
63 """
64 try:
65 if isinstance(message, six.text_type):
66 message = message.encode('utf-8')
67 crypto.verify(self._pubkey, signature, message, 'sha256')
68 return True
69 except:
70 return False
71
72 @staticmethod
74 """Construct a Verified instance from a string.
75
76 Args:
77 key_pem: string, public key in PEM format.
78 is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
79 expected to be an RSA key in PEM format.
80
81 Returns:
82 Verifier instance.
83
84 Raises:
85 OpenSSL.crypto.Error if the key_pem can't be parsed.
86 """
87 if is_x509_cert:
88 pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
89 else:
90 pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
91 return OpenSSLVerifier(pubkey)
92
95 """Signs messages with a private key."""
96
98 """Constructor.
99
100 Args:
101 pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
102 """
103 self._key = pkey
104
105 - def sign(self, message):
106 """Signs a message.
107
108 Args:
109 message: bytes, Message to be signed.
110
111 Returns:
112 string, The signature of the message for the given key.
113 """
114 if isinstance(message, six.text_type):
115 message = message.encode('utf-8')
116 return crypto.sign(self._key, message, 'sha256')
117
118 @staticmethod
120 """Construct a Signer instance from a string.
121
122 Args:
123 key: string, private key in PKCS12 or PEM format.
124 password: string, password for the private key file.
125
126 Returns:
127 Signer instance.
128
129 Raises:
130 OpenSSL.crypto.Error if the key can't be parsed.
131 """
132 parsed_pem_key = _parse_pem_key(key)
133 if parsed_pem_key:
134 pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
135 else:
136 if isinstance(password, six.text_type):
137 password = password.encode('utf-8')
138 pkey = crypto.load_pkcs12(key, password).get_privatekey()
139 return OpenSSLSigner(pkey)
140
141 except ImportError:
142 OpenSSLVerifier = None
143 OpenSSLSigner = None
144
145
146 try:
147 from Crypto.PublicKey import RSA
148 from Crypto.Hash import SHA256
149 from Crypto.Signature import PKCS1_v1_5
150 from Crypto.Util.asn1 import DerSequence
154 """Verifies the signature on a message."""
155
157 """Constructor.
158
159 Args:
160 pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with.
161 """
162 self._pubkey = pubkey
163
164 - def verify(self, message, signature):
165 """Verifies a message against a signature.
166
167 Args:
168 message: string, The message to verify.
169 signature: string, The signature on the message.
170
171 Returns:
172 True if message was signed by the private key associated with the public
173 key that this object was constructed with.
174 """
175 try:
176 return PKCS1_v1_5.new(self._pubkey).verify(
177 SHA256.new(message), signature)
178 except:
179 return False
180
181 @staticmethod
183 """Construct a Verified instance from a string.
184
185 Args:
186 key_pem: string, public key in PEM format.
187 is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
188 expected to be an RSA key in PEM format.
189
190 Returns:
191 Verifier instance.
192 """
193 if is_x509_cert:
194 if isinstance(key_pem, six.text_type):
195 key_pem = key_pem.encode('ascii')
196 pemLines = key_pem.replace(b' ', b'').split()
197 certDer = _urlsafe_b64decode(b''.join(pemLines[1:-1]))
198 certSeq = DerSequence()
199 certSeq.decode(certDer)
200 tbsSeq = DerSequence()
201 tbsSeq.decode(certSeq[0])
202 pubkey = RSA.importKey(tbsSeq[6])
203 else:
204 pubkey = RSA.importKey(key_pem)
205 return PyCryptoVerifier(pubkey)
206
209 """Signs messages with a private key."""
210
212 """Constructor.
213
214 Args:
215 pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
216 """
217 self._key = pkey
218
219 - def sign(self, message):
220 """Signs a message.
221
222 Args:
223 message: string, Message to be signed.
224
225 Returns:
226 string, The signature of the message for the given key.
227 """
228 if isinstance(message, six.text_type):
229 message = message.encode('utf-8')
230 return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
231
232 @staticmethod
234 """Construct a Signer instance from a string.
235
236 Args:
237 key: string, private key in PEM format.
238 password: string, password for private key file. Unused for PEM files.
239
240 Returns:
241 Signer instance.
242
243 Raises:
244 NotImplementedError if they key isn't in PEM format.
245 """
246 parsed_pem_key = _parse_pem_key(key)
247 if parsed_pem_key:
248 pkey = RSA.importKey(parsed_pem_key)
249 else:
250 raise NotImplementedError(
251 'PKCS12 format is not supported by the PyCrypto library. '
252 'Try converting to a "PEM" '
253 '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) '
254 'or using PyOpenSSL if native code is an option.')
255 return PyCryptoSigner(pkey)
256
257 except ImportError:
258 PyCryptoVerifier = None
259 PyCryptoSigner = None
260
261
262 if OpenSSLSigner:
263 Signer = OpenSSLSigner
264 Verifier = OpenSSLVerifier
265 elif PyCryptoSigner:
266 Signer = PyCryptoSigner
267 Verifier = PyCryptoVerifier
268 else:
269 raise ImportError('No encryption library found. Please install either '
270 'PyOpenSSL, or PyCrypto 2.6 or later')
274 """Identify and extract PEM keys.
275
276 Determines whether the given key is in the format of PEM key, and extracts
277 the relevant part of the key if it is.
278
279 Args:
280 raw_key_input: The contents of a private key file (either PEM or PKCS12).
281
282 Returns:
283 string, The actual key if the contents are from a PEM file, or else None.
284 """
285 offset = raw_key_input.find(b'-----BEGIN ')
286 if offset != -1:
287 return raw_key_input[offset:]
288
291 if isinstance(raw_bytes, six.text_type):
292 raw_bytes = raw_bytes.encode('utf-8')
293 return base64.urlsafe_b64encode(raw_bytes).decode('ascii').rstrip('=')
294
297
298 if isinstance(b64string, six.text_type):
299 b64string = b64string.encode('ascii')
300 padded = b64string + b'=' * (4 - len(b64string) % 4)
301 return base64.urlsafe_b64decode(padded)
302
305 return json.dumps(data, separators=(',', ':'))
306
309 """Make a signed JWT.
310
311 See http://self-issued.info/docs/draft-jones-json-web-token.html.
312
313 Args:
314 signer: crypt.Signer, Cryptographic signer.
315 payload: dict, Dictionary of data to convert to JSON and then sign.
316
317 Returns:
318 string, The JWT for the payload.
319 """
320 header = {'typ': 'JWT', 'alg': 'RS256'}
321
322 segments = [
323 _urlsafe_b64encode(_json_encode(header)),
324 _urlsafe_b64encode(_json_encode(payload)),
325 ]
326 signing_input = '.'.join(segments)
327
328 signature = signer.sign(signing_input)
329 segments.append(_urlsafe_b64encode(signature))
330
331 logger.debug(str(segments))
332
333 return '.'.join(segments)
334
337 """Verify a JWT against public certs.
338
339 See http://self-issued.info/docs/draft-jones-json-web-token.html.
340
341 Args:
342 jwt: string, A JWT.
343 certs: dict, Dictionary where values of public keys in PEM format.
344 audience: string, The audience, 'aud', that this JWT should contain. If
345 None then the JWT's 'aud' parameter is not verified.
346
347 Returns:
348 dict, The deserialized JSON payload in the JWT.
349
350 Raises:
351 AppIdentityError if any checks are failed.
352 """
353 segments = jwt.split('.')
354
355 if len(segments) != 3:
356 raise AppIdentityError('Wrong number of segments in token: %s' % jwt)
357 signed = '%s.%s' % (segments[0], segments[1])
358
359 signature = _urlsafe_b64decode(segments[2])
360
361
362 json_body = _urlsafe_b64decode(segments[1])
363 try:
364 parsed = json.loads(json_body.decode('utf-8'))
365 except:
366 raise AppIdentityError('Can\'t parse token: %s' % json_body)
367
368
369 verified = False
370 for pem in certs.values():
371 verifier = Verifier.from_string(pem, True)
372 if verifier.verify(signed, signature):
373 verified = True
374 break
375 if not verified:
376 raise AppIdentityError('Invalid token signature: %s' % jwt)
377
378
379 iat = parsed.get('iat')
380 if iat is None:
381 raise AppIdentityError('No iat field in token: %s' % json_body)
382 earliest = iat - CLOCK_SKEW_SECS
383
384
385 now = int(time.time())
386 exp = parsed.get('exp')
387 if exp is None:
388 raise AppIdentityError('No exp field in token: %s' % json_body)
389 if exp >= now + MAX_TOKEN_LIFETIME_SECS:
390 raise AppIdentityError('exp field too far in future: %s' % json_body)
391 latest = exp + CLOCK_SKEW_SECS
392
393 if now < earliest:
394 raise AppIdentityError('Token used too early, %d < %d: %s' %
395 (now, earliest, json_body))
396 if now > latest:
397 raise AppIdentityError('Token used too late, %d > %d: %s' %
398 (now, latest, json_body))
399
400
401 if audience is not None:
402 aud = parsed.get('aud')
403 if aud is None:
404 raise AppIdentityError('No aud field in token: %s' % json_body)
405 if aud != audience:
406 raise AppIdentityError('Wrong recipient, %s != %s: %s' %
407 (aud, audience, json_body))
408
409 return parsed
410