Remove ed25519, add pynacl, documentation updates and preparation for PyPI

This commit is contained in:
= 2019-09-14 02:38:56 -05:00
parent f000e564f9
commit fff2d3e244
Signed by: kiichan
GPG key ID: 619DFD67F0976616
24 changed files with 404 additions and 82 deletions

14
AUTHORS.md Normal file
View file

@ -0,0 +1,14 @@
---
title: Credits
---
Development Lead
================
- Miroslav Shubernetskiy - <https://github.com/miki725>
Contributors
============
- Keaton Brown - <https://gitlab.com/WolfgangAxel>
- Python 2 removal, Django 2.2 upgrade

13
AUTHORS.rst Normal file
View file

@ -0,0 +1,13 @@
Credits
-------
Development Lead
~~~~~~~~~~~~~~~~
* Miroslav Shubernetskiy - https://github.com/miki725
Contributors
~~~~~~~~~~~~
* Keaton Brown - https://gitlab.com/WolfgangAxel
* Python 2 removal, Django 2.2 upgrade

View file

@ -1,3 +1,24 @@
Sat, 14 Sep 2019 02:38:56 -0500
Keaton <kii-chan@tutanota.com>
Remove ed25519, add pynacl, documentation updates and preparation for PyPI
- Fixed typos in readme, fixed formatting to be a little nicer.
- Converted `tests` "app". Run tests as follows:
python3 tests/manage.py test
- `python-ed25519` lists on its GitHub page that it is depreciated, and that
`pynacl` is the recommended alternative. As such, I've converted all calls
to the ed25519 library into pynacl calls. All the tests pass, so we *should*
be good...
- Sidenote, I should really get in the habit of making sure tests pass before
committing...
- Started adding files needed by PyPI. Honestly I'm not sure that everything
is 100% necessary, but I'm not really concerning myself with it for this
commit. I just want to push the
--------------------
Wed, 04 Sep 2019 21:08:57 -0500
Keaton <kii-chan@tutanota.com>
Misc updates

13
HISTORY.md Normal file
View file

@ -0,0 +1,13 @@
---
title: History
---
0.2.0 (2019-??-??)
==================
- First release of rewrite on PyPI.
0.1.0 (2015-05-20)
==================
- First release on PyPI.

14
HISTORY.rst Normal file
View file

@ -0,0 +1,14 @@
.. :changelog:
History
-------
0.2.0 (2019-09-20)
~~~~~~~~~~~~~~~~~~
* First release of rewrite on PyPI.
0.1.0 (2015-05-20)
~~~~~~~~~~~~~~~~~~
* First release on PyPI.

25
LICENSE.md Normal file
View file

@ -0,0 +1,25 @@
---
title: License
---
The MIT License (MIT)
Copyright (c) 2015, Miroslav Shubernetskiy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -22,7 +22,9 @@ so all credit for this working belongs with them.
First step is to install `django-sqrl-2` which is easies to do using pip:
$ python3 -m pip install #django-sqrl-2
```
$ #python3 -m pip install django-sqrl-2
```
### Django settings
@ -30,28 +32,34 @@ Once installed there are a few required changes in Django settings:
* Make sure that some required Django apps are used:
INSTALLED_APPS = [
...,
'sqrl',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.staticfiles',
]
```
INSTALLED_APPS = [
...,
'sqrl',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.staticfiles',
]
```
* Make sure that some required Django middleware are used:
MIDDLEWARE_CLASSES = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
]
```
MIDDLEWARE_CLASSES = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
]
```
* Change `AUTHENTICATION_BACKENDS` to use SQRL backend vs Django's
`ModelBackend` (default):
AUTHENTICATION_BACKENDS = [
'sqrl.backends.SQRLModelBackend',
]
```
AUTHENTICATION_BACKENDS = [
'sqrl.backends.SQRLModelBackend',
]
```
* If you are using Django admin, following are required:
@ -59,46 +67,55 @@ Once installed there are a few required changes in Django settings:
This allows Django to prioritize `sqrl` templates since `django-sqrl`
overwrites some of them.
INSTALLED_APPS = [
...,
'sqrl',
'django.contrib.admin',
...
]
```
INSTALLED_APPS = [
...,
'sqrl',
'django.contrib.admin',
...
]
```
* Make sure to add a custom template directory in settings. `django-sqrl`
extends Django admin's `base.html` which by default causes infinite recursion.
To solve that, simply add a custom template directory which allows `django-sqrl`
to explicitly extend from `django.contrib.admin` `base.html` template:
import os
import django
TEMPLATE_DIRS = [
os.path.dirname(django.__file__),
]
```
import os
import django
TEMPLATE_DIRS = [
os.path.dirname(django.__file__),
]
```
## URLs
All of SQRL functionality is enabled by adding its URLs to the root URL config:
from django.urls import path, include
```
from django.urls import path, include
urlpatterns = [
...
path('sqrl/', include('sqrl.urls', namespace="sqrl")),
...
]
urlpatterns = [
...
path('sqrl/', include('sqrl.urls', namespace="sqrl")),
...
]
```
If you use Django admin, the `/admin/sqrl_manage` will be available to manage
If you use Django admin, the `/admin/sqrl_manage` endpoint will be available to manage
your site's SQRL identities.
## Templates
Now that SQRL is installed in your Django project, you can use it on any login
page with three simple template tag:
page with three simple template tags:
{% load sqrl %}
{% sqrl as sqrl_session %}
{% sqrl_login_dropin sqrl_session [[a named redirect]] %}
```
{% load sqrl %}
{% sqrl as sqrl_session %}
{% sqrl_login_dropin sqrl_session [[a named redirect]] %}
```
The [[named redirect]] is the page that should be redirected to after logging
in. Any name that can be resolved by django's `reverse` function will work (i.e.
@ -112,13 +129,15 @@ These three tags will add a simple element to your login page:
If that doesn't suit your fancy, you may build your own template from the
following essential tags:
{% load sqrl %}
{% sqrl as sqrl_session %}
<a href="{{ sqrl_session.sqrl_url }}">
<div id="sqrl-qr" data-sqrl="{{ sqrl_session.sqrl_url }}"></div>
</a>
<script>SQRL_NEXT="{{ your desired redirect }}"; SQRL_CHECK_URL="{% sqrl_status_url_script_tag sqrl_session %}"</script>
<script type="application/javascript" src="{% static 'sqrl/sqrl.js' %}"></script>
```
{% load sqrl %}
{% sqrl as sqrl_session %}
<a href="{{ sqrl_session.sqrl_url }}">
<div id="sqrl-qr" data-sqrl="{{ sqrl_session.sqrl_url }}"></div>
</a>
<script>SQRL_NEXT="{{ your desired redirect }}"; SQRL_CHECK_URL="{% sqrl_status_url_script_tag sqrl_session %}"</script>
<script type="application/javascript" src="{% static 'sqrl/sqrl.js' %}"></script>
```
## Management Command
@ -126,19 +145,27 @@ SQRL uses server state to keep track of open SQRL transactions in order to
mitigate replay attacks. Since this state will constantly grow if not cleared,
`django-sqrl` provides a helper management command to clear expired states:
$ python3 manage.py clearsqrlnuts
```
$ python3 manage.py clearsqrlnuts
```
It is recommended to run this command as repeating task. Here is an example
configuration for `cron`:
*/5 * * * * python manage.py clearsqrlnuts >/dev/null 2>&1
```
*/5 * * * * python manage.py clearsqrlnuts >/dev/null 2>&1
```
## ~~Testing~~
~~To run the tests, you need to install the testing requirements first:~~
$ #make install
```
$ #make install
```
~~Then to run the tests, you can use use the Makefile command:~~
$ #make test
```
$ #make test
```

12
requirements-dev.txt Normal file
View file

@ -0,0 +1,12 @@
-r requirements.txt
coverage
django-extensions
django-sslserver
flake8
mock
Sphinx
sphinx-autobuild
sphinx-rtd-theme
tox
watchdog
Werkzeug

View file

@ -1,3 +1,3 @@
Django
django
django-braces
ed25519
pynacl

55
setup.py Normal file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from setuptools import setup, find_packages
from sqrl import __author__, __version__
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname), 'r').read()
authors = read('AUTHORS.md')
history = read('HISTORY.md')
licence = read('LICENSE.md')
readme = read('README.md')
requirements = read('requirements.txt').splitlines() + [
'setuptools',
]
test_requirements = (
read('requirements.txt').splitlines()
+ read('requirements-dev.txt').splitlines()[1:]
)
setup(
name='django-sqrl-2',
version=__version__,
author=__author__,
description='SQRL authentication support for Django',
long_description='\n\n'.join([readme, history, authors, licence]),
long_description_content_type='text/markdown',
url='https://gitlub.com/WolfgangAxel/django-sqrl-2',
license='MIT',
packages=find_packages(exclude=['tests', 'tests.*']),
install_requires=requirements,
test_suite='tests',
tests_require=test_requirements,
keywords=' '.join([
'django-sqrl',
'django-sqrl-2',
'sqrl'
]),
classifiers=[
'Intended Audience :: Developers',
'Natural Language :: English',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Development Status :: 2 - Pre-Alpha',
],
python_requires='>=3.7',
)

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
__author__ = 'Miroslav Shubernetskiy | Keaton Brown'
__email__ = 'miroslav@miki725.com | kii-chan@tutanota.com'
__version__ = '0.1.0'
__author__ = 'Keaton Brown'
__email__ = 'kii-chan@tutanota.com'
__version__ = '0.2.0'

View file

@ -3,7 +3,8 @@
from collections import OrderedDict
import os
import ed25519
from nacl import signing
from nacl.exceptions import BadSignatureError
from django.utils.crypto import constant_time_compare, salted_hmac
from .utils import Base64, Encoder
@ -89,6 +90,8 @@ class Ed25519(object):
def __init__(self, public_key, private_key, msg):
self.public_key = public_key
self.private_key = private_key
if private_key and type(private_key) == bytes:
self.private_key = private_key[:32]
self.msg = msg
def is_signature_valid(self, other_signature):
@ -101,10 +104,10 @@ class Ed25519(object):
Boolean indicating whether validation has succeeded.
"""
try:
vk = ed25519.VerifyingKey(self.public_key)
vk.verify(other_signature, self.msg)
vk = signing.VerifyKey(self.public_key)
vk.verify(self.msg, other_signature)
return True
except (AssertionError, ed25519.BadSignatureError):
except (AssertionError, BadSignatureError) as e:
return False
def sign_data(self):
@ -116,8 +119,8 @@ class Ed25519(object):
bytes
ed25519 signature
"""
sk = ed25519.SigningKey(self.private_key)
return sk.sign(self.msg)
sk = signing.SigningKey(self.private_key)
return sk.sign(self.msg).signature
def generate_randomness(size=32):

View file

@ -12,7 +12,7 @@
</p>
{% sqrl as sqrl_session %}
{% sqrl_login_dropin sqrl_session login %}
{% sqrl_login_dropin sqrl_session "sqrl:login" %}
{% endblock %}
{% comment %}
@ -23,5 +23,6 @@ All necessary data should already be in the context.
Please note again that this template is for SQRL-exclusive logins.
If you would like to add SQRL login to an existing login page,
you should rather adjust that template as it is probably way more involved.
you should rather adjust that template as it is probably way more involved
to add that here.
{% endcomment %}

View file

@ -15,7 +15,7 @@ def sqrl(context):
@register.inclusion_tag('sqrl/sqrl-dropin.html')
def sqrl_login_dropin(sqrl_session, redir=""):
def sqrl_login_dropin(sqrl_session, redir="sqrl:login"):
"""
Creates a drop-in SQRL element in your template pages.
Add it to your login template to make it SQRL-aware.

View file

@ -4,7 +4,8 @@ from collections import OrderedDict
from django.conf import settings
import os
import ed25519
from nacl import signing
from nacl.exceptions import BadSignatureError
import mock
from ..crypto import HMAC, Ed25519, generate_randomness
@ -111,11 +112,11 @@ class TestEd25519(unittest.TestCase):
b'\x97\x145\x90N[\xb9\xfc\x8e\x8a\x9e\xd2=\xad\x84\xcd\xf1\x93\x06'
)
@mock.patch('ed25519.SigningKey')
@mock.patch('nacl.signing.SigningKey')
def test_sign_data_mock(self, mock_signing_key):
signature = self.sig.sign_data()
self.assertEqual(signature, mock_signing_key.return_value.sign.return_value)
self.assertEqual(signature, mock_signing_key.return_value.sign.return_value.signature)
mock_signing_key.assert_called_once_with(self.sig.private_key)
mock_signing_key.return_value.sign.assert_called_once_with(self.sig.msg)
@ -125,17 +126,17 @@ class TestEd25519(unittest.TestCase):
self.assertTrue(self.sig.is_signature_valid(signature))
self.assertFalse(self.sig.is_signature_valid(b'a' + signature[:-1]))
@mock.patch('ed25519.VerifyingKey')
@mock.patch('nacl.signing.VerifyKey')
def test_is_signature_mock(self, mock_verifying_key):
is_valid = self.sig.is_signature_valid(mock.sentinel.signature)
self.assertTrue(is_valid)
mock_verifying_key.assert_called_once_with(self.sig.public_key)
mock_verifying_key.return_value.verify.assert_called_once_with(
mock.sentinel.signature, self.data
self.data, mock.sentinel.signature
)
@mock.patch('ed25519.VerifyingKey')
@mock.patch('nacl.signing.VerifyKey')
def test_is_signature_mock_assertion_error(self, mock_verifying_key):
mock_verifying_key.return_value.verify.side_effect = AssertionError
@ -144,19 +145,19 @@ class TestEd25519(unittest.TestCase):
self.assertFalse(is_valid)
mock_verifying_key.assert_called_once_with(self.sig.public_key)
mock_verifying_key.return_value.verify.assert_called_once_with(
mock.sentinel.signature, self.data
self.data, mock.sentinel.signature
)
@mock.patch('ed25519.VerifyingKey')
def test_is_signature_mock_bas_signature_error(self, mock_verifying_key):
mock_verifying_key.return_value.verify.side_effect = ed25519.BadSignatureError
@mock.patch('nacl.signing.VerifyKey')
def test_is_signature_mock_bad_signature_error(self, mock_verifying_key):
mock_verifying_key.return_value.verify.side_effect = BadSignatureError
is_valid = self.sig.is_signature_valid(mock.sentinel.signature)
self.assertFalse(is_valid)
mock_verifying_key.assert_called_once_with(self.sig.public_key)
mock_verifying_key.return_value.verify.assert_called_once_with(
mock.sentinel.signature, self.data
self.data, mock.sentinel.signature
)

View file

@ -2,7 +2,7 @@
import unittest
from collections import OrderedDict
import ed25519
from nacl import signing
import mock
from django import forms, test
from django.contrib.auth import SESSION_KEY, get_user_model
@ -19,9 +19,10 @@ TESTING_MODULE = 'sqrl.forms'
class TestRequestForm(test.TestCase):
def get_key_pair(self):
signing_key, verifying_key = ed25519.create_keypair()
signing_key = signing_key.to_bytes()
verifying_key = verifying_key.to_bytes()
signing_key = signing.SigningKey.generate()
verifying_key = signing_key.verify_key
signing_key = signing_key._signing_key
verifying_key = verifying_key._key
return signing_key, verifying_key

View file

@ -59,7 +59,7 @@ class TestSQRLStatusView(test.TestCase):
def test_get_success_url_complete_registration(self):
self.view.request.GET['url'] = '?next={}'.format(reverse('sqrl:login'))
self.view.request.user.is_authenticated.return_value = False
self.view.request.user.is_authenticated = False
self.view.request.session = {SQRL_IDENTITY_SESSION_KEY: ''}
self.assertEqual(

View file

@ -16,7 +16,7 @@ urlpatterns = [
path("auth/", SQRLAuthView.as_view(), name="auth"),
path("login/", SQRLLoginView.as_view(), name="login"),
path("manage/", SQRLIdentityManagementView.as_view(), name='manage'),
path("register/",SQRLCompleteRegistrationView.as_view(), name='complete-registration'),
path("register/", SQRLCompleteRegistrationView.as_view(), name='complete-registration'),
re_path(r"^status/(?P<transaction>[A-Za-z0-9_-]{43})/$", SQRLStatusView.as_view(), name='status'),
path('admin/sqrl_manage/', AdminSiteSQRLIdentityManagementView.as_view(), name='admin-sqrl_manage'),
]

1
tests/__init__.py Normal file
View file

@ -0,0 +1 @@

95
tests/settings.py Normal file
View file

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
"""
Bare ``settings.py`` for running tests for rest_framework_bulk
"""
import os
import django
DEBUG = True
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.dirname(django.__file__),],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
INSTALLED_APPS = (
'sqrl',
'tests',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.staticfiles',
)
AUTHENTICATION_BACKENDS = (
'sqrl.backends.SQRLModelBackend',
)
SQRL_SERVER_FRIENDLY_NAME = 'Django SQRL Test Site'
STATIC_URL = '/static/'
SECRET_KEY = 'foo'
ROOT_URLCONF = 'tests.urls'
LOGIN_REDIRECT_URL = '/'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
},
'loggers': {
'sqrl': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
'django.request': {
'handlers': ['console'],
'level': 'ERROR',
'propagate': False,
},
},
}

4
tests/static/sqrl/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,3 @@
{% load static %}
<!doctype html>
<html lang="en">
@ -39,6 +37,12 @@
{% endblock content %}
</div>
<div id="footer">
{% block footer %}
SQRL Test server. Originally created by <a href="https://github.com/miki725">miki725</a>. Revised by <a href="https://gitlab.com/WolfgangAxel">WolfgangAxel</a>
{% endblock %}
</div>
{% block scripts %}
{% endblock %}

View file

@ -0,0 +1 @@
{% extends 'base.html' %}

17
tests/urls.py Normal file
View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from django.urls import path, include
from django.contrib import admin
from django.contrib.auth import urls as auth_urlpatterns
from django.contrib.auth.views import LogoutView
from django.views.generic import TemplateView
urlpatterns = [
path("", TemplateView.as_view(template_name='sqrl.html'), name='index'),
path("admin/", admin.site.urls),
path("sqrl/", include('sqrl.urls', namespace='sqrl')),
path("logout/", LogoutView.as_view(), {'next_page': 'sqrl:login'}, name='logout'),
# Doesn't this not work/break things?
path("", include(auth_urlpatterns)),
]