commit 388ddb783c7bff39feea87dd7e2ac0bfd410d82a Author: John Dennis Date: Thu Mar 16 10:18:57 2017 -0400 Use jwcrypto instead of jwt * Replace all imports of jwt with jwcrypto, continue to rely on cryptography * Update doc replacing jwt with jwcrypto * Update dependencies and requirements * Add utility functions get_rsa_public_key() and get_rsa_private_key() to obtain cryptography's RSAPublicKey and RSAPrivateKey * Add utility normalize_claims() replicate type conversion jwt offered, primarially datatime conversion * Refactor to use common code and separate concerns * Replace jwt's RSA-SHA1 signature with primitives from cryptography Signed-off-by: John Dennis diff --git a/oauthlib/common.py b/oauthlib/common.py index 5d999b2..5fc0a3c 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -15,6 +15,16 @@ import random import re import sys import time +from calendar import timegm + +from jwcrypto.jwk import JWK +from jwcrypto.jwt import JWT +from jwcrypto.common import json_decode +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, load_pem_public_key) +from cryptography.hazmat.primitives.asymmetric.rsa import ( + RSAPrivateKey, RSAPublicKey) try: from urllib import quote as _quote @@ -229,28 +239,69 @@ def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET): return ''.join(rand.choice(chars) for x in range(length)) -def generate_signed_token(private_pem, request): - import jwt +def get_rsa_private_key(data): + if isinstance(data, (bytes_type, unicode_type)): + if isinstance(data, unicode_type): + data = data.encode('ascii') + # load_pem_private_key() requires the data to be str or bytes + private_key = load_pem_private_key(data, None, default_backend()) + else: + private_key = data + + if not isinstance(private_key, RSAPrivateKey): + raise TypeError("Expected RSAPrivateKey, but got %s" % + private_key.__class__.__name__) + + return private_key + + +def get_rsa_public_key(data): + if isinstance(data, (bytes_type, unicode_type)): + if isinstance(data, unicode_type): + data = data.encode('ascii') + # load_pem_public_key() requires the data to be str or bytes + public_key = load_pem_public_key(data, default_backend()) + else: + public_key = data + + if not isinstance(public_key, RSAPublicKey): + raise TypeError("Expected RSAPublicKey, but got %s" % + public_key.__class__.__name__) + + return public_key - now = datetime.datetime.utcnow() +def normalize_claims(claims): + for claim in ['exp', 'iat', 'nbf']: + # Convert datetime to a intDate value in known time-format claims + if isinstance(claims.get(claim), datetime.datetime): + claims[claim] = timegm(claims[claim].utctimetuple()) + return claims + + +def generate_jwt_assertion(private_key, claims): + rsa_private_key = get_rsa_private_key(private_key) + jwkey = JWK.from_pyca(rsa_private_key) + token = JWT(header={'alg': 'RS256'}, claims=normalize_claims(claims)) + token.make_signed_token(jwkey) + return to_unicode(token.serialize(), "UTF-8") + + +def generate_signed_token(private_pem, request): + now = datetime.datetime.utcnow() claims = { 'scope': request.scope, 'exp': now + datetime.timedelta(seconds=request.expires_in) } - claims.update(request.claims) + return generate_jwt_assertion(private_pem, claims) - token = jwt.encode(claims, private_pem, 'RS256') - token = to_unicode(token, "UTF-8") - - return token - - -def verify_signed_token(public_pem, token): - import jwt - return jwt.decode(token, public_pem, algorithms=['RS256']) +def verify_signed_token(public_key, token): + rsa_public_key = get_rsa_public_key(public_key) + jwkey = JWK.from_pyca(rsa_public_key) + signed_token = JWT(key=jwkey, jwt=token) + return json_decode(signed_token.claims) def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET): diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 8fa22ba..6956732 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -31,8 +31,15 @@ try: except ImportError: import urllib.parse as urlparse from . import utils -from oauthlib.common import urldecode, extract_params, safe_string_equals -from oauthlib.common import bytes_type, unicode_type + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding +from jwcrypto.jwk import JWK +from jwcrypto.jws import JWS + +from oauthlib.common import (urldecode, extract_params, safe_string_equals, + bytes_type, unicode_type, + get_rsa_private_key, get_rsa_public_key) def construct_base_string(http_method, base_string_uri, @@ -464,17 +471,8 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8 return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8') -_jwtrs1 = None - -#jwt has some nice pycrypto/cryptography abstractions -def _jwt_rs1_signing_algorithm(): - global _jwtrs1 - if _jwtrs1 is None: - import jwt.algorithms as jwtalgo - _jwtrs1 = jwtalgo.RSAAlgorithm(jwtalgo.hashes.SHA1) - return _jwtrs1 -def sign_rsa_sha1(base_string, rsa_private_key): +def sign_rsa_sha1(base_string, private_key): """**RSA-SHA1** Per `section 3.4.3`_ of the spec. @@ -493,10 +491,11 @@ def sign_rsa_sha1(base_string, rsa_private_key): if isinstance(base_string, unicode_type): base_string = base_string.encode('utf-8') # TODO: finish RSA documentation - alg = _jwt_rs1_signing_algorithm() - key = _prepare_key_plus(alg, rsa_private_key) - s=alg.sign(base_string, key) - return binascii.b2a_base64(s)[:-1].decode('utf-8') + key = get_rsa_private_key(private_key) + signer = key.signer(padding.PKCS1v15(), hashes.SHA1()) + signer.update(base_string) + signature = signer.finalize() + return binascii.b2a_base64(signature)[:-1].decode('utf-8') def sign_rsa_sha1_with_client(base_string, client): @@ -568,17 +567,13 @@ def verify_hmac_sha1(request, client_secret=None, resource_owner_secret) return safe_string_equals(signature, request.signature) -def _prepare_key_plus(alg, keystr): - if isinstance(keystr, bytes_type): - keystr = keystr.decode('utf-8') - return alg.prepare_key(keystr) def verify_rsa_sha1(request, rsa_public_key): """Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature. Per `section 3.4.3`_ of the spec. - Note this method requires the jwt and cryptography libraries. + Note this method requires the cryptography library. .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3 @@ -595,9 +590,14 @@ def verify_rsa_sha1(request, rsa_public_key): message = construct_base_string(request.http_method, uri, norm_params).encode('utf-8') sig = binascii.a2b_base64(request.signature.encode('utf-8')) - alg = _jwt_rs1_signing_algorithm() - key = _prepare_key_plus(alg, rsa_public_key) - return alg.verify(message, key, sig) + key = get_rsa_public_key(rsa_public_key) + verifier = key.verifier(sig, padding.PKCS1v15(), hashes.SHA1()) + verifier.update(message) + try: + verifier.verify() + return True + except InvalidSignature: + return False def verify_plaintext(request, client_secret=None, resource_owner_secret=None): diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 36da98b..d974b1d 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -10,7 +10,7 @@ from __future__ import absolute_import, unicode_literals import time -from oauthlib.common import to_unicode +from oauthlib.common import generate_jwt_assertion from .base import Client from ..parameters import prepare_token_request @@ -139,7 +139,6 @@ class ServiceApplicationClient(Client): .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 """ - import jwt key = private_key or self.private_key if not key: @@ -166,8 +165,7 @@ class ServiceApplicationClient(Client): claim.update(extra_claims or {}) - assertion = jwt.encode(claim, key, 'RS256') - assertion = to_unicode(assertion) + assertion = generate_jwt_assertion(key, claim) return prepare_token_request(self.grant_type, body=body, diff --git a/setup.py b/setup.py index 718db43..77cd6c8 100755 --- a/setup.py +++ b/setup.py @@ -18,11 +18,11 @@ def fread(fn): return f.read() if sys.version_info[0] == 3: - tests_require = ['nose', 'cryptography', 'pyjwt>=1.0.0', 'blinker'] + tests_require = ['nose', 'cryptography', 'jwcrypto>=0.3.2', 'blinker'] else: - tests_require = ['nose', 'unittest2', 'cryptography', 'mock', 'pyjwt>=1.0.0', 'blinker'] + tests_require = ['nose', 'unittest2', 'cryptography', 'mock', 'jwcrypto>=0.3.2', 'blinker'] rsa_require = ['cryptography'] -signedtoken_require = ['cryptography', 'pyjwt>=1.0.0'] +signedtoken_require = ['cryptography', 'jwcrypto>=0.3.2'] signals_require = ['blinker'] requires = [] diff --git a/tests/oauth2/rfc6749/clients/test_service_application.py b/tests/oauth2/rfc6749/clients/test_service_application.py index de57291..842bbcb 100644 --- a/tests/oauth2/rfc6749/clients/test_service_application.py +++ b/tests/oauth2/rfc6749/clients/test_service_application.py @@ -4,10 +4,9 @@ from __future__ import absolute_import, unicode_literals import os from time import time -import jwt from mock import patch -from oauthlib.common import Request +from oauthlib.common import Request, verify_signed_token from oauthlib.oauth2 import ServiceApplicationClient from ....unittest import TestCase @@ -92,10 +91,10 @@ mfvGGg3xNjTMO7IdrwIDAQAB self.assertEqual(r.isnot, 'empty') self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) - claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256']) + claim = verify_signed_token(self.public_key, r.assertion) self.assertEqual(claim['iss'], self.issuer) - # audience verification is handled during decode now + self.assertEqual(claim['aud'], self.audience) self.assertEqual(claim['sub'], self.subject) self.assertEqual(claim['iat'], int(t.return_value)) diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index aff0d84..850d73e 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, unicode_literals from ...unittest import TestCase import json -import jwt import mock from oauthlib import common