-
-
Notifications
You must be signed in to change notification settings - Fork 240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
replace pyjwkest and cryptodome with pyjwt and cryptography #208
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,10 @@ | |
import time | ||
import uuid | ||
|
||
from Cryptodome.PublicKey.RSA import importKey | ||
from django.utils import dateformat, timezone | ||
from jwkest.jwk import RSAKey as jwk_RSAKey | ||
from jwkest.jwk import SYMKey | ||
from jwkest.jws import JWS | ||
from jwkest.jwt import JWT | ||
import jwt | ||
from cryptography.hazmat.primitives import serialization | ||
from cryptography.hazmat.backends import default_backend | ||
|
||
from oidc_provider.lib.utils.common import get_issuer | ||
from oidc_provider.models import ( | ||
|
@@ -71,27 +69,25 @@ def encode_id_token(payload, client): | |
Represent the ID Token as a JSON Web Token (JWT). | ||
Return a hash. | ||
""" | ||
keys = get_client_alg_keys(client) | ||
_jws = JWS(payload, alg=client.jwt_alg) | ||
return _jws.sign_compact(keys) | ||
|
||
|
||
def decode_id_token(token, client): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function was completely unused and so I removed it. |
||
""" | ||
Represent the ID Token as a JSON Web Token (JWT). | ||
Return a hash. | ||
""" | ||
keys = get_client_alg_keys(client) | ||
return JWS().verify_compact(token, keys=keys) | ||
key = client.client_secret | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the same: if the algorithm isn't RS256, then the key is the client's secret, otherwise it's the first private key in the database. |
||
if client.jwt_alg == 'RS256': | ||
rsakeys = RSAKey.objects.all() | ||
if not rsakeys: | ||
raise Exception('You must have an RSA Key.') | ||
rsakey = rsakeys[0] | ||
key = serialization.load_pem_private_key(rsakey.key.encode(), None, default_backend()).private_bytes( | ||
encoding=serialization.Encoding.PEM, | ||
format=serialization.PrivateFormat.TraditionalOpenSSL, | ||
encryption_algorithm=serialization.NoEncryption()).decode('utf8') | ||
return jwt.encode(payload, key, algorithm=client.jwt_alg).decode() | ||
|
||
|
||
def client_id_from_id_token(id_token): | ||
""" | ||
Extracts the client id from a JSON Web Token (JWT). | ||
Returns a string or None. | ||
""" | ||
payload = JWT().unpack(id_token).payload() | ||
return payload.get('aud', None) | ||
return jwt.decode(id_token, verify=False).get('aud', None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically the same thing: from an id_token extract the aud. We set verify to False, because the provider is not the audience of this JWT. |
||
|
||
|
||
def create_token(user, client, scope, id_token_dic=None): | ||
|
@@ -138,22 +134,3 @@ def create_code(user, client, scope, nonce, is_authentication, | |
code.is_authentication = is_authentication | ||
|
||
return code | ||
|
||
|
||
def get_client_alg_keys(client): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is no longer necessary as the way to get the key has been simplified. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By removing this, and switching to the new 'simplified' method, explicit checking for specific algorithms is lost, which opens the door to future issues. I would much prefer to see explicit checking added back - even if it adds some code complexity, the greater clarity in code paths and explicit exception for other algorithms is worth the cost. |
||
""" | ||
Takes a client and returns the set of keys associated with it. | ||
Returns a list of keys. | ||
""" | ||
if client.jwt_alg == 'RS256': | ||
keys = [] | ||
for rsakey in RSAKey.objects.all(): | ||
keys.append(jwk_RSAKey(key=importKey(rsakey.key), kid=rsakey.kid)) | ||
if not keys: | ||
raise Exception('You must add at least one RSA Key.') | ||
elif client.jwt_alg == 'HS256': | ||
keys = [SYMKey(key=client.client_secret, alg=client.jwt_alg)] | ||
else: | ||
raise Exception('Unsupported key algorithm.') | ||
|
||
return keys |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
from Cryptodome.PublicKey import RSA | ||
from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key | ||
from cryptography.hazmat.primitives import serialization | ||
from cryptography.hazmat.backends import default_backend | ||
from django.core.management.base import BaseCommand | ||
|
||
from oidc_provider.models import RSAKey | ||
|
@@ -9,8 +11,12 @@ class Command(BaseCommand): | |
|
||
def handle(self, *args, **options): | ||
try: | ||
key = RSA.generate(1024) | ||
rsakey = RSAKey(key=key.exportKey('PEM').decode('utf8')) | ||
key = generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The public exponent is the same as the default in pycryptodome. I've upped the default bitsize of the key to 2048 instead of 1024. The key is generated, then serialized into the database in the PEM format. It's a little weird that we always have to specify the default_backend(), but that's for old reasons: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like seeing the move to 2048 bit RSA - 1024 is too weak for use today. Anything less than 2048 should be avoided if at all possible (see for example the move away from 1024 by Web PKI). |
||
rsakey = RSAKey( | ||
key=key.private_bytes( | ||
encoding=serialization.Encoding.PEM, | ||
format=serialization.PrivateFormat.TraditionalOpenSSL, | ||
encryption_algorithm=serialization.NoEncryption()).decode('utf8')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, there would be support for encrypted private keys, to reduce the risk of exposing the private key - this would add a defense in depth layer, though would add somewhat to configuration complexity. This would be a nice improvement over the current state. |
||
rsakey.save() | ||
self.stdout.write(u'RSA key successfully created with kid: {0}'.format(rsakey.kid)) | ||
except Exception as e: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,16 +6,17 @@ | |
except ImportError: | ||
from urllib.parse import urlsplit, parse_qs, urlunsplit, urlencode | ||
|
||
from Cryptodome.PublicKey import RSA | ||
from base64 import urlsafe_b64encode | ||
from django.contrib.auth.views import ( | ||
redirect_to_login, | ||
logout, | ||
) | ||
|
||
import django | ||
if django.VERSION >= (1, 11): | ||
from struct import pack | ||
|
||
try: | ||
from django.urls import reverse | ||
else: | ||
except ImportError: | ||
from django.core.urlresolvers import reverse | ||
|
||
from django.contrib.auth import logout as django_user_logout | ||
|
@@ -26,7 +27,9 @@ | |
from django.views.decorators.clickjacking import xframe_options_exempt | ||
from django.views.decorators.http import require_http_methods | ||
from django.views.generic import View | ||
from jwkest import long_to_base64 | ||
|
||
from cryptography.hazmat.primitives.serialization import load_pem_private_key | ||
from cryptography.hazmat.backends import default_backend | ||
|
||
from oidc_provider.lib.claims import StandardScopeClaims | ||
from oidc_provider.lib.endpoints.authorize import AuthorizeEndpoint | ||
|
@@ -284,18 +287,29 @@ def get(self, request, *args, **kwargs): | |
|
||
|
||
class JwksView(View): | ||
def _num_to_b64_string(self, value): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think pyjwt actually provides an interface for generating a JWK from a key, so I'll change that in a future PR. For now, this is just the way JWK specifies how to give your public key. |
||
int_array = [] | ||
while value: | ||
value, r = divmod(value, 256) | ||
int_array.insert(0, r) | ||
char_array = pack('B'*len(int_array), *int_array) | ||
return urlsafe_b64encode(char_array).rstrip(b'=').decode("ascii") | ||
|
||
def get(self, request, *args, **kwargs): | ||
dic = dict(keys=[]) | ||
|
||
for rsakey in RSAKey.objects.all(): | ||
public_key = RSA.importKey(rsakey.key).publickey() | ||
public_numbers = load_pem_private_key( | ||
rsakey.key.encode(), | ||
None, | ||
default_backend()).public_key().public_numbers() | ||
dic['keys'].append({ | ||
'kty': 'RSA', | ||
'alg': 'RS256', | ||
'use': 'sig', | ||
'kid': rsakey.kid, | ||
'n': long_to_base64(public_key.n), | ||
'e': long_to_base64(public_key.e), | ||
'n': self._num_to_b64_string(public_numbers.n), | ||
'e': self._num_to_b64_string(public_numbers.e), | ||
}) | ||
|
||
response = JsonResponse(dic) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,10 +38,13 @@ | |
test_suite='runtests.runtests', | ||
tests_require=[ | ||
'pyjwkest>=1.3.0', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kept pyjwkest in the tests to show that this would all be compatible. Just want to show I'm not adding or removing functionality, just refactoring some libraries out. |
||
'cryptography>==2.0', | ||
'pyjwt>==1.5.0', | ||
'mock>=2.0.0', | ||
], | ||
|
||
install_requires=[ | ||
'pyjwkest>=1.3.0', | ||
'cryptography>==2.0', | ||
'pyjwt>==1.5.0', | ||
], | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pep8 compliance change.