642 lines
19 KiB
Python
642 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
import unittest
|
|
from collections import OrderedDict
|
|
|
|
import ed25519
|
|
import mock
|
|
from django import forms, test
|
|
from django.contrib.auth import SESSION_KEY, get_user_model
|
|
from django.utils.timezone import now
|
|
|
|
from ..crypto import HMAC, Ed25519, generate_randomness
|
|
from ..forms import PasswordLessUserCreationForm, RequestForm
|
|
from ..models import SQRLIdentity, SQRLNut
|
|
from ..utils import Base64, Encoder
|
|
|
|
|
|
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()
|
|
|
|
return signing_key, verifying_key
|
|
|
|
def _setup(self):
|
|
hasattr(self, 'nut') and self.nut.delete()
|
|
hasattr(self, 'user') and self.user.delete()
|
|
hasattr(self, 'identity') and self.identity.delete()
|
|
|
|
self.nut = SQRLNut(
|
|
nonce=generate_randomness(),
|
|
transaction_nonce=generate_randomness(),
|
|
session_key=generate_randomness(20),
|
|
is_transaction_complete=False,
|
|
ip_address='127.0.0.1',
|
|
timestamp=now(),
|
|
)
|
|
self.nut.save()
|
|
|
|
self.user = get_user_model().objects.create(
|
|
username='test_clean_session',
|
|
)
|
|
|
|
self.identity = SQRLIdentity(
|
|
user_id=self.user.pk,
|
|
public_key=Base64.encode(self.public_key),
|
|
server_unlock_key=Base64.encode(self.server_unlock_key),
|
|
verify_unlock_key=Base64.encode(self.verify_unlock_key),
|
|
is_enabled=True,
|
|
is_only_sqrl=False,
|
|
)
|
|
self.identity.save()
|
|
|
|
self.server_data = OrderedDict([
|
|
('ver', 1),
|
|
('nut', self.nut.nonce),
|
|
('tif', '8'),
|
|
('qry', '/sqrl/auth/?nut=nonce'),
|
|
('sfn', 'Test Server'),
|
|
])
|
|
self.server_data['mac'] = HMAC(self.nut, self.server_data).sign_data()
|
|
self.server_data = Encoder.normalize(self.server_data)
|
|
|
|
self.client_data = OrderedDict([
|
|
('ver', 1),
|
|
('cmd', self.cmd),
|
|
('opt', ['sqrlonly']),
|
|
])
|
|
if self.include_idk:
|
|
self.client_data['idk'] = self.public_key
|
|
if self.include_pidk:
|
|
self.client_data['pidk'] = self.previous_public_key
|
|
if self.include_suk:
|
|
self.client_data['suk'] = self.server_unlock_key
|
|
if self.include_vuk:
|
|
self.client_data['vuk'] = self.verify_unlock_key
|
|
|
|
self.payload_client_data = Encoder.normalize(OrderedDict(
|
|
(k, v if not isinstance(v, list) else '~'.join(v))
|
|
for k, v in self.client_data.items()
|
|
))
|
|
|
|
self.data = {
|
|
'client': Encoder.base64_dumps(self.client_data),
|
|
'server': Encoder.base64_dumps(self.server_data),
|
|
}
|
|
self.signable_data = (
|
|
self.data['client'] + self.data['server']
|
|
).encode('ascii')
|
|
if self.include_ids:
|
|
self.data['ids'] = Ed25519(
|
|
self.public_key, self.identity_key, self.signable_data
|
|
).sign_data()
|
|
if self.include_pids:
|
|
self.data['pids'] = Ed25519(
|
|
self.previous_public_key, self.previous_identity_key, self.signable_data
|
|
).sign_data()
|
|
if self.include_urs:
|
|
self.data['urs'] = Ed25519(
|
|
self.verify_unlock_key, self.unlock_key, self.signable_data
|
|
).sign_data()
|
|
|
|
self.cleaned_data = self.data.copy()
|
|
self.cleaned_data.update({
|
|
'client': self.client_data,
|
|
'server': self.server_data,
|
|
})
|
|
|
|
self.form = RequestForm(self.nut, data=self.data)
|
|
|
|
def setUp(self):
|
|
super(TestRequestForm, self).setUp()
|
|
|
|
self.cmd = ['query']
|
|
|
|
self.identity_key, self.public_key = self.get_key_pair()
|
|
self.previous_identity_key, self.previous_public_key = self.get_key_pair()
|
|
self.unlock_key, self.verify_unlock_key = self.get_key_pair()
|
|
self.server_unlock_key = b'hello'
|
|
|
|
self.include_idk = True
|
|
self.include_pidk = True
|
|
self.include_suk = True
|
|
self.include_vuk = True
|
|
self.include_ids = True
|
|
self.include_pids = True
|
|
self.include_urs = True
|
|
|
|
self._setup()
|
|
|
|
def tearDown(self):
|
|
self.user and self.user.delete()
|
|
self.identity and self.identity.delete()
|
|
self.nut and self.nut.delete()
|
|
super(TestRequestForm, self).tearDown()
|
|
|
|
def test_init(self):
|
|
form = RequestForm(mock.sentinel.nut)
|
|
|
|
self.assertEqual(form.nut, mock.sentinel.nut)
|
|
self.assertIsNone(form.session)
|
|
self.assertIsNone(form.identity)
|
|
self.assertIsNone(form.previous_identity)
|
|
|
|
def test_clean_client(self):
|
|
self.form.cleaned_data = {'client': self.payload_client_data}
|
|
|
|
self.assertEqual(self.form.clean_client(), dict(self.client_data))
|
|
|
|
def test_clean_client_invalid(self):
|
|
self.form.cleaned_data = {
|
|
'client': {
|
|
'ver': '2',
|
|
}
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form.clean_client()
|
|
|
|
def test_clean_server_not_dict(self):
|
|
self.form.cleaned_data = {'server': mock.sentinel.server_data}
|
|
|
|
self.assertEqual(self.form.clean_server(), mock.sentinel.server_data)
|
|
|
|
def test_clean_server(self):
|
|
self.form.cleaned_data = {
|
|
'server': self.server_data
|
|
}
|
|
|
|
self.assertEqual(
|
|
self.form.clean_server(),
|
|
self.server_data
|
|
)
|
|
|
|
def test_clean_server_mac_not_base64(self):
|
|
self.server_data['mac'] = 'hello'
|
|
self.form.cleaned_data = {'server': self.server_data}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form.clean_server()
|
|
|
|
def test_clean_server_mismatch_nut(self):
|
|
self.server_data['nut'] = self.server_data['nut'][::-1]
|
|
self.form.cleaned_data = {'server': self.server_data}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form.clean_server()
|
|
|
|
def test_clean_server_mismatch_missing_mac(self):
|
|
del self.server_data['mac']
|
|
self.form.cleaned_data = {'server': self.server_data}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form.clean_server()
|
|
|
|
def test_clean_server_invalid_mac(self):
|
|
self.server_data['mac'] = self.server_data['mac'][::-1]
|
|
self.form.cleaned_data = {'server': self.server_data}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form.clean_server()
|
|
|
|
def test_clean_ids(self):
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
'ids': self.data['ids'],
|
|
}
|
|
|
|
self.assertEqual(
|
|
self.form.clean_ids(),
|
|
self.data['ids']
|
|
)
|
|
|
|
def test_clean_ids_invalid(self):
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
'ids': self.data['ids'][::-1],
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form.clean_ids()
|
|
|
|
def test_clean_pids_valid(self):
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
'pids': self.data['pids'],
|
|
}
|
|
|
|
self.assertEqual(
|
|
self.form.clean_pids(),
|
|
self.data['pids']
|
|
)
|
|
|
|
def test_clean_pids_invalid(self):
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
'pids': self.data['pids'][::-1],
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form.clean_pids()
|
|
|
|
def test_clean_pids_missing_pids(self):
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form.clean_pids()
|
|
|
|
def test_clean_pids_missing_pidk(self):
|
|
self.client_data.pop('pidk')
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
'pids': self.data['pids'],
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form.clean_pids()
|
|
|
|
def test_clean_urs(self):
|
|
self.form.cleaned_data = self.cleaned_data
|
|
self.form.identity = self.identity
|
|
|
|
self.assertEqual(
|
|
self.form._clean_urs(),
|
|
self.data['urs']
|
|
)
|
|
|
|
def test_clean_urs_invalid(self):
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
'urs': self.data['urs'][::-1],
|
|
}
|
|
self.form.identity = self.identity
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_urs()
|
|
|
|
def test_clean_urs_no_suk(self):
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
'urs': self.data['urs'],
|
|
}
|
|
self.form.identity = self.identity
|
|
self.identity.server_unlock_key = None
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_urs()
|
|
|
|
def test_clean_urs_no_vuk(self):
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
'urs': self.data['urs'],
|
|
}
|
|
self.form.identity = self.identity
|
|
self.identity.verify_unlock_key = None
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_urs()
|
|
|
|
def test_clean_cmd_query(self):
|
|
self.cmd = ['query']
|
|
self._setup()
|
|
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
self.assertIsNone(self.form._clean_client_cmd())
|
|
|
|
def test_clean_cmd_ident(self):
|
|
self.cmd = ['ident']
|
|
self._setup()
|
|
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
self.assertIsNone(self.form._clean_client_cmd())
|
|
|
|
def test_clean_cmd_ident_no_suk_vuk_without_identity(self):
|
|
self.cmd = ['ident']
|
|
self.include_suk = None
|
|
self.include_vuk = None
|
|
self._setup()
|
|
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_ident_suk_vuk_with_identity(self):
|
|
self.cmd = ['ident']
|
|
self._setup()
|
|
|
|
self.form.identity = self.identity
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_ident_with_disable(self):
|
|
self.cmd = ['ident', 'disable']
|
|
self._setup()
|
|
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_ident_no_urs_with_previous_identity(self):
|
|
self.cmd = ['ident']
|
|
self._setup()
|
|
|
|
self.form.previous_identity = self.identity
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_disable(self):
|
|
self.cmd = ['disable']
|
|
self._setup()
|
|
|
|
self.form.identity = self.identity
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
self.assertIsNone(self.form._clean_client_cmd())
|
|
|
|
def test_clean_cmd_disable_no_identity(self):
|
|
self.cmd = ['disable']
|
|
self._setup()
|
|
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_disable_with_enable(self):
|
|
self.cmd = ['disable', 'enable']
|
|
self._setup()
|
|
|
|
self.form.identity = self.identity
|
|
self.form.cleaned_data = {
|
|
'client': self.client_data,
|
|
}
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_enable(self):
|
|
self.cmd = ['enable']
|
|
self._setup()
|
|
|
|
self.form.identity = self.identity
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
self.assertIsNone(self.form._clean_client_cmd())
|
|
|
|
def test_clean_cmd_enable_no_urs(self):
|
|
self.cmd = ['enable']
|
|
self.include_urs = False
|
|
self._setup()
|
|
|
|
self.form.identity = self.identity
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_enable_no_identity(self):
|
|
self.cmd = ['enable']
|
|
self._setup()
|
|
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_enable_with_disable(self):
|
|
self.cmd = ['enable', 'disable']
|
|
self._setup()
|
|
|
|
self.form.identity = self.identity
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_remove(self):
|
|
self.cmd = ['remove']
|
|
self._setup()
|
|
|
|
self.form.identity = self.identity
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
self.assertIsNone(self.form._clean_client_cmd())
|
|
|
|
def test_clean_cmd_remove_no_identity(self):
|
|
self.cmd = ['remove']
|
|
self._setup()
|
|
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_remove_no_urs(self):
|
|
self.cmd = ['remove']
|
|
self.include_urs = False
|
|
self._setup()
|
|
|
|
self.form.identity = self.identity
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_cmd_remove_with_other_cmd(self):
|
|
self.cmd = ['remove', 'ident']
|
|
self._setup()
|
|
|
|
self.form.identity = self.identity
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_client_cmd()
|
|
|
|
def test_clean_session_empty(self):
|
|
self.form.session = {}
|
|
|
|
self.assertIsNone(self.form._clean_session())
|
|
|
|
def test_clean_session_user_not_found(self):
|
|
assert not get_user_model().objects.filter(pk=1000).first()
|
|
|
|
self.form.session = {
|
|
SESSION_KEY: '1000',
|
|
}
|
|
|
|
self.assertIsNone(self.form._clean_session())
|
|
|
|
def test_clean_session_user_not_int(self):
|
|
self.form.session = {
|
|
SESSION_KEY: 'aaa',
|
|
}
|
|
|
|
self.assertIsNone(self.form._clean_session())
|
|
|
|
def test_clean_session_no_sqrl_identity(self):
|
|
self.identity.delete()
|
|
self.identity = None
|
|
self.form.session = {
|
|
SESSION_KEY: str(self.user.pk),
|
|
}
|
|
|
|
self.assertIsNone(self.form._clean_session())
|
|
|
|
def test_clean_session_public_key_not_matches(self):
|
|
self.identity.public_key = self.identity.public_key[::-1]
|
|
self.identity.save()
|
|
|
|
self.form.session = {
|
|
SESSION_KEY: str(self.user.pk),
|
|
}
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_session()
|
|
|
|
def test_clean_session_user_code_mismatch(self):
|
|
self.form.identity = self.identity
|
|
self.form.session = {
|
|
SESSION_KEY: str(self.user.pk + 1),
|
|
}
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
with self.assertRaises(forms.ValidationError):
|
|
self.form._clean_session()
|
|
|
|
def test_clean(self):
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
actual = self.form.clean()
|
|
|
|
self.assertEqual(actual, self.cleaned_data)
|
|
|
|
@mock.patch.object(RequestForm, 'find_identities')
|
|
@mock.patch.object(RequestForm, '_clean_client_cmd')
|
|
@mock.patch.object(RequestForm, '_clean_urs')
|
|
@mock.patch.object(RequestForm, 'find_session')
|
|
@mock.patch.object(RequestForm, '_clean_session')
|
|
@mock.patch.object(forms.Form, 'clean')
|
|
def test_clean_mock(self,
|
|
mock_super_clean,
|
|
mock_clean_session,
|
|
mock_find_session,
|
|
mock_clean_urs,
|
|
mock_clean_client_cmd,
|
|
mock_find_identities):
|
|
mock_super_clean.return_value = mock.sentinel.cleaned_data
|
|
|
|
actual = self.form.clean()
|
|
|
|
self.assertEqual(actual, mock.sentinel.cleaned_data)
|
|
mock_super_clean.assert_called_once_with()
|
|
mock_clean_session.assert_called_once_with()
|
|
mock_find_session.assert_called_once_with()
|
|
mock_clean_urs.assert_called_once_with()
|
|
mock_clean_client_cmd.assert_called_once_with()
|
|
mock_find_identities.assert_called_once_with()
|
|
|
|
@mock.patch(TESTING_MODULE + '.SessionMiddleware')
|
|
def test_find_session(self, mock_session_middleware):
|
|
self.form.find_session()
|
|
|
|
self.assertEqual(
|
|
self.form.session,
|
|
mock_session_middleware.return_value.SessionStore.return_value
|
|
)
|
|
mock_session_middleware.return_value.SessionStore.assert_called_once_with(
|
|
self.nut.session_key,
|
|
)
|
|
|
|
def test_find_identities(self):
|
|
self.form.cleaned_data = self.cleaned_data
|
|
|
|
self.form.find_identities()
|
|
|
|
self.assertIsNotNone(self.form.identity)
|
|
self.assertIsInstance(self.form.identity, SQRLIdentity)
|
|
self.assertEqual(self.form.identity.public_key, self.identity.public_key)
|
|
self.assertIsNone(self.form.previous_identity)
|
|
|
|
@mock.patch.object(RequestForm, '_get_identity')
|
|
def test_find_identities_mock(self, mock_get_identity):
|
|
self.form.cleaned_data = self.cleaned_data
|
|
mock_get_identity.side_effect = mock.sentinel.identity, mock.sentinel.previous_identity
|
|
|
|
self.form.find_identities()
|
|
|
|
self.assertEqual(self.form.identity, mock.sentinel.identity)
|
|
self.assertEqual(self.form.previous_identity, mock.sentinel.previous_identity)
|
|
mock_get_identity.assert_has_calls([
|
|
mock.call(self.public_key),
|
|
mock.call(self.previous_public_key),
|
|
])
|
|
|
|
@mock.patch(TESTING_MODULE + '.SQRLIdentity')
|
|
def test_get_identity(self, mock_sqrl_identity):
|
|
actual = self.form._get_identity(self.public_key)
|
|
|
|
self.assertEqual(
|
|
actual,
|
|
mock_sqrl_identity.objects.filter.return_value.first.return_value
|
|
)
|
|
mock_sqrl_identity.objects.filter.assert_called_once_with(
|
|
public_key=Base64.encode(self.public_key)
|
|
)
|
|
|
|
def test_get_identity_no_key(self):
|
|
self.assertIsNone(self.form._get_identity(None))
|
|
|
|
|
|
class TestRandomPasswordUserCreationForm(unittest.TestCase):
|
|
def test_init(self):
|
|
self.assertIn('password1', PasswordLessUserCreationForm.base_fields)
|
|
self.assertIn('password2', PasswordLessUserCreationForm.base_fields)
|
|
|
|
form = PasswordLessUserCreationForm()
|
|
|
|
self.assertNotIn('password1', form.fields)
|
|
self.assertNotIn('password2', form.fields)
|
|
|
|
def test_save(self):
|
|
form = PasswordLessUserCreationForm({'username': 'test'})
|
|
|
|
self.assertTrue(form.is_valid())
|
|
|
|
user = form.save()
|
|
|
|
self.assertEqual(user.username, 'test')
|
|
self.assertTrue(user.password.startswith('!'))
|
|
|
|
user.delete()
|