├── python └── pqauth │ ├── __init__.py │ ├── pqauth_django_server │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── client.pub │ │ ├── evil.key.pub │ │ ├── client.key.pub │ │ ├── settings.py │ │ ├── evil.key │ │ ├── server.key │ │ ├── client.key │ │ └── protocol.py │ ├── admin.py │ ├── urls.py │ ├── keys.py │ ├── fixtures │ │ └── test_accounts.json │ ├── models.py │ └── views.py │ ├── requirements.txt │ ├── crypto.py │ └── client.py ├── .gitignore ├── LICENSE └── README.md /python/pqauth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/pqauth/requirements.txt: -------------------------------------------------------------------------------- 1 | pycrypto==2.6 2 | paramiko==1.10.1 3 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .protocol import ProtocolTest 2 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from django_pqauth_server.models import PublicKey 4 | from django_pqauth_server.models import PQAuthSession 5 | 6 | admin.site.register(PublicKey) 7 | admin.site.register(PQAuthSession) 8 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from pqauth.pqauth_django_server import views 4 | 5 | urlpatterns = patterns( 6 | "", 7 | url(r"^public-key", views.public_key), 8 | url(r"^hello", views.hello), 9 | url(r"^confirm", views.confirm) 10 | ) 11 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/tests/client.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDjghIwGSjVuWFpHvUQY+6geNwILvy6kvS9LaenjF+xLcs5jADGRbE4u7mdEPcIrX2rd0gV8NE/x/MM1BJRMfbAkCl5i1FAl2Apt4KkyvNfHT7dBD8lXn0w09P4pKUmgAet57LX4djL0RIQIQMNuUdV0cIOPNhIsFoZlybsmWBGK+BhCu6R21TYZSQ8DRPY4ymj0EzGVU6U0jCpT0QSX2vz8XE4AckVO+8bt2xk8bSo1AI4UAWgGO4vUEzL+g4ZmHqMSKzTrL/T3nwB1a83Vr7W4La2/XW4mxKBlW6ZLQ+LvFVFYbX5kybk27vp3865kDnalSEZyPcwHQml9tB1b7K7 2 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/tests/evil.key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQChoTQrdeMOd3EELrJWUTWVvsH/pZCkSUrdaa1y0eddTKn75nNR2U6n1pRayCV76Aw5xXeNHeazazlTxb5fkyPNMSTIUE3LYfd9H7xNClnMQlwqJBDAedRsRu6vyhUAs8E9tEeEoteei3IG0iMBCa2nIAdUkajVznw4Rb38kbTrlVD4+CqgjQ8R0WVHqyl5pIr1RnWqwZtVeJE1p2Ul8yqMM5Q4m37jDyyv110U8FBCTuzmqhaojkqkliRfRAl40BMVSAAAl4Klc1NrGFeSUdj77uvOHaxoz4Z9WOr5RA+r6nA66nKeIcpLD3tpMYvxrbFuuorDIFJL536ZnN8IvjqN ted@focus 2 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/tests/client.key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDjghIwGSjVuWFpHvUQY+6geNwILvy6kvS9LaenjF+xLcs5jADGRbE4u7mdEPcIrX2rd0gV8NE/x/MM1BJRMfbAkCl5i1FAl2Apt4KkyvNfHT7dBD8lXn0w09P4pKUmgAet57LX4djL0RIQIQMNuUdV0cIOPNhIsFoZlybsmWBGK+BhCu6R21TYZSQ8DRPY4ymj0EzGVU6U0jCpT0QSX2vz8XE4AckVO+8bt2xk8bSo1AI4UAWgGO4vUEzL+g4ZmHqMSKzTrL/T3nwB1a83Vr7W4La2/XW4mxKBlW6ZLQ+LvFVFYbX5kybk27vp3865kDnalSEZyPcwHQml9tB1b7K7 ted@focus 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Ted's 38 | lint 39 | *.sqlite -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DIRNAME = os.path.dirname(__file__) 4 | 5 | DATABASES = { 6 | "default": { 7 | "ENGINE": "django.db.backends.sqlite3" 8 | } 9 | } 10 | 11 | INSTALLED_APPS = ( 12 | "django.contrib.auth", 13 | "django.contrib.contenttypes", 14 | "pqauth.pqauth_django_server" 15 | ) 16 | 17 | SECRET_KEY = "chicken butt" 18 | 19 | PQAUTH_SERVER_KEY = os.path.join(DIRNAME, "server.key") 20 | 21 | ROOT_URLCONF = "pqauth.pqauth_django_server.urls" 22 | 23 | 24 | TEST_CLIENT_KEY = os.path.join(DIRNAME, "client.key") 25 | TEST_EVIL_KEY = os.path.join(DIRNAME, "evil.key") 26 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/keys.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | from pqauth.crypto import load_key_file 5 | from pqauth.crypto import public_key_fingerprint 6 | 7 | def load_server_key(): 8 | try: 9 | key_path = settings.PQAUTH_SERVER_KEY 10 | except AttributeError: 11 | msg = "You must set settings.PQUATH_SERVER_KEY" 12 | raise ImproperlyConfigured(msg) 13 | 14 | key_password = None 15 | try: 16 | key_password = settings.PQAUTH_SERVER_KEY_PASSWORD 17 | except AttributeError: 18 | pass 19 | 20 | return load_key_file(key_path, key_password) 21 | 22 | SERVER_KEY = load_server_key() 23 | SERVER_KEY_FINGERPRINT = public_key_fingerprint(SERVER_KEY) 24 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/fixtures/test_accounts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "model": "auth.user", 3 | "pk": 1, 4 | "fields": { 5 | "username": "api-user", 6 | "password": "!" 7 | } 8 | }, 9 | { 10 | "model": "pqauth_django_server.PublicKey", 11 | "pk": "de:96:14:9e:99:04:97:16:4b:08:99:30:1a:0b:51:18", 12 | "fields": { 13 | "user": 1, 14 | "ssh_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDjghIwGSjVuWFpHvUQY+6geNwILvy6kvS9LaenjF+xLcs5jADGRbE4u7mdEPcIrX2rd0gV8NE/x/MM1BJRMfbAkCl5i1FAl2Apt4KkyvNfHT7dBD8lXn0w09P4pKUmgAet57LX4djL0RIQIQMNuUdV0cIOPNhIsFoZlybsmWBGK+BhCu6R21TYZSQ8DRPY4ymj0EzGVU6U0jCpT0QSX2vz8XE4AckVO+8bt2xk8bSo1AI4UAWgGO4vUEzL+g4ZmHqMSKzTrL/T3nwB1a83Vr7W4La2/XW4mxKBlW6ZLQ+LvFVFYbX5kybk27vp3865kDnalSEZyPcwHQml9tB1b7K7" 15 | } 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Ted Dziuba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | from pqauth import crypto 4 | 5 | 6 | class PublicKey(models.Model): 7 | user = models.ForeignKey(User, related_name="public_keys") 8 | 9 | # keys MD5-fingerprint to 47 characters, including colons for readability 10 | fingerprint = models.CharField(max_length=64, primary_key=True) 11 | ssh_key = models.TextField() 12 | 13 | @property 14 | def public_key(self): 15 | return crypto.rsa_key(self.ssh_key) 16 | 17 | def __unicode__(self): 18 | return self.fingerprint 19 | 20 | 21 | class PQAuthSession(models.Model): 22 | server_guid = models.CharField(max_length=32, primary_key=True) 23 | client_guid = models.CharField(max_length=32) 24 | session_key = models.CharField(max_length=65, unique=True, 25 | null=True, blank=True) 26 | expires = models.DateTimeField(null=True, blank=True) 27 | 28 | user = models.ForeignKey(User, related_name="pqauth_sessions") 29 | 30 | def __unicode__(self): 31 | if not self.session_key: 32 | return "[Negotiating] %s, %s" % (self.server_guid, self.client_guid) 33 | else: 34 | return "[Established] %s" % self.session_key 35 | -------------------------------------------------------------------------------- /python/pqauth/crypto.py: -------------------------------------------------------------------------------- 1 | from binascii import hexlify 2 | import uuid 3 | 4 | from Crypto.Cipher import PKCS1_OAEP 5 | from Crypto.PublicKey import RSA 6 | from Crypto.Random import random 7 | from paramiko.rsakey import RSAKey as ParamikoRSAKey 8 | 9 | 10 | def load_key_file(path, passphrase=None): 11 | f = open(path, "rb") 12 | try: 13 | return rsa_key(f.read(), passphrase) 14 | finally: 15 | f.close() 16 | 17 | 18 | def rsa_key(text, passphrase=None): 19 | return RSA.importKey(text, passphrase) 20 | 21 | 22 | def public_key_fingerprint(key): 23 | # paramiko can compute the OpenSSH-style fingerprint 24 | # Only fingerprints the public key 25 | 26 | paramiko_key = ParamikoRSAKey(vals=(key.e, key.n)) 27 | fp = hexlify(paramiko_key.get_fingerprint()) 28 | 29 | # OpenSSH puts a ":" character between every pair of hex-digits. 30 | # For whatever reason. Readability, I guess. 31 | openssh_fp = ":".join([a+b for a, b in zip(fp[::2], fp[1::2])]) 32 | 33 | return openssh_fp 34 | 35 | 36 | def rsa_encrypt(plaintext, receiver_public_key): 37 | cipher = PKCS1_OAEP.new(receiver_public_key) 38 | return cipher.encrypt(plaintext) 39 | 40 | 41 | def rsa_decrypt(ciphertext, receiver_private_key): 42 | cipher = PKCS1_OAEP.new(receiver_private_key) 43 | return cipher.decrypt(ciphertext) 44 | 45 | 46 | def random_guid(): 47 | secure_random = random.getrandbits(128) 48 | random_uuid = uuid.UUID(int=secure_random) 49 | return str(random_uuid) 50 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/tests/evil.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAoaE0K3XjDndxBC6yVlE1lb7B/6WQpElK3WmtctHnXUyp++Zz 3 | UdlOp9aUWsgle+gMOcV3jR3ms2s5U8W+X5MjzTEkyFBNy2H3fR+8TQpZzEJcKiQQ 4 | wHnUbEbur8oVALPBPbRHhKLXnotyBtIjAQmtpyAHVJGo1c58OEW9/JG065VQ+Pgq 5 | oI0PEdFlR6speaSK9UZ1qsGbVXiRNadlJfMqjDOUOJt+4w8sr9ddFPBQQk7s5qoW 6 | qI5KpJYkX0QJeNATFUgAAJeCpXNTaxhXklHY++7rzh2saM+GfVjq+UQPq+pwOupy 7 | niHKSw97aTGL8a2xbrqKwyBSS+d+mZzfCL46jQIDAQABAoIBABGU8JDdtQJI2eFj 8 | lwCuus58PqwpfW9xjZRCP5zi2nEaus7tBZRcuCKnw+GQLgupdVL/eP6/xu2zdzv2 9 | obvRzK4wb1je62d5U6unvRsASj5e5Zmr7KqTVhklKiezKs+1vgqRRspV0HCtqIy3 10 | ZRlizSIF6OY5Jg2D9Z1FcOI9k1mg6zOjTnPrjeMlzNPOjwFgYxD/wXMeLrB214xo 11 | +jinzz9xNuW5BFJstklEg2O/sq3lwkFfXmzxatj/Axz/xt+af3CL7vjBlcBYAyje 12 | BB2DY4yartOHEQ+FMMeYNKFchqSV3HoI34r41SeIZlDfsbAgVeK5FSXHT0+H0Sz3 13 | Ehq9Cx0CgYEA0jjCj0iReE7L8okpBp9ndy1TFpi5a6fPjvy99tY7LgBqqVY+ZRes 14 | o9geWvuQuhQMT6CWr47fcAjDlgQo9bERaasgPQBWEXhxKj1UFKXYP35K06tqcpqX 15 | l2c116xIfQZMX1Asj8wZ3qzHHKgzQNivU2DV378qWT6jytmkeBQQVDsCgYEAxNOV 16 | Flyvyl/r/TnYHFG7WwxB2pF9FEGNkr/pzaD7LU1kpUH1UfosiWr+1DJ4ma2wGPEg 17 | hFsCIs7kHKpcwrfuYlkMhgmbqjPyjtfme+SzTd2C7yeyBQh9qOQLJseAORWQ0Czy 18 | VEzvrUQX+CZwEia/BbbioDYumWTqVWMZXV/vp9cCgYB7kcezp6lyuYaik0NdVDfi 19 | Uu8jOjTL9kFIQIJ9ZKI2wSHxVfOpQshFKxV0z6jb6ErwR2or2eS3Gz+0n89f1Gxy 20 | 25dApEDzVgPXG2OgG3hIojjjwT/6C6sN4SpCE1AsxZYA9MSs5gFS5ebIjopKFNTC 21 | QwqU0e7XtL1c0KAfwVNz3wKBgGDpwTXSi2f9FQiJS8Sd5b9t6JsDGfA4WsoQHsH1 22 | 6tcijVTlhjJIGVfMTA8VjtY0dEnDqHwjB4k2D5GhvKzPvdvE9uqknnYOv/bfjYgh 23 | UrwbPpYdGIVr6duX9Xmxr4vr93LZPrSNcVIB/j9cRcBaPaJFq46xPv6edtd2RQ35 24 | 59nfAoGAL7TNx2lKF5EZasuGDBMdDClbTQoYq1+W04S+CZAwAl82GjPbmE/u4hDi 25 | FE0GMmW0n3UF3qor3BMfLDBWmZrV45bifahhBRSHe3s+Mv1jnvrut2UP77vEoHw2 26 | u8if/YIE1DJQl0QKYqhmi9uAeLuOnXUcdOVFk+LFAYNa/oLPX1Q= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/tests/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAxy5s8dmFMyflc9SsfdY+9g1Nf6qq4Bakzp4IYytZ5UiGI3LO 3 | XpJAmV7bZULsYfg7kSmkyzrKkkG7FBPssJPhmQw5722eDlKDuyCGVYULeAMY/p50 4 | lKtl4jXGVOMX23olPFd9zADqnf/ZmbzeHafGFOGeF8Q+9Hv6htMTgQrjmIQvspV8 5 | ZAhnuE7zHtZoTB1ZM2fNy2IIzJlkvQ6p67mNczUX313yJWF9Wykw2e4uhESur+o0 6 | iG+8rnmBH7ejI9gUhh5yRA/l9GJuLxhOSzDAdx17oBARHZRqdKF25aB8XSYtLB2D 7 | lowF85CZK0WgwFGFVA5Grsitru86F04W/h70TwIDAQABAoIBAQCb0ucqQdkiDzlD 8 | 6ALWI3F4Pyn9EcDCtRRUDHBVXQnkBVvjiaKHe/WLxxju3G9fqbq4MxMYZzMpPsEj 9 | 0P6fmeGpQVZlyKUZYVZrY+OULhSt3AaG0+IymPCJCzbRHCSC8MkGrw/cNG9YElvj 10 | GU3Pd3zQwz1SLJZv4Do7lhAxKRqrKoSu5pKIaXETzbO9h4c0sa1bdJSckhG+RqVS 11 | EcKCyDFpO/cB1GsJcejNrV6JzbA8xly7K8H7zbd8l3LMDCN8hW0K+2Di2DKQ3Fyn 12 | qwdW9OCTbwsUBtd+1alIiuZGQoqWMPBYnyFpX5HShRObE/MZDEextsiSji5jfRIq 13 | 7gOggRVRAoGBAPEV9Z7Ighpx+6XhNuvVvhmOzdmNXeQ5kMVhsbCaRqZUGJmeCUTs 14 | YXbl6dKCowzr950sV5lOrS0ZZOGNsY/YSmcXy1ebr1GhR0uFup1c7EZDNtgvY/O2 15 | Rk2/Nm1O3chB5vofy+i7yMh5PqdN9CB7oHIu/ita0+oPtU1l8jW5xT5JAoGBANOA 16 | 1OzGzLtTbF/NxtkRX2nCR9FQnznrA8VvfWfxKfFqRSuqbtIxK9VO56Ob7BPtM0gZ 17 | wIHF6snj06GqPtbH3xjD2+FPpl7dDNFTAJher33Fb570vA4dyUpt4urJK+xjxMgP 18 | Bct8DdiAnKP8m3FQZ1A4WUnxDfMSpNfRFshxnn3XAoGAaG+g1UX6xkX2Q2eKiSMw 19 | lJW+Kq4IrGMbicXGWcCbNlZbycAiN89GcWErp1ucEm7t9xJMaEci0dScVPEyqCOE 20 | J6CxH6R7kBsTbW3i52RnnhBj48azv9GIJwncJAH0JVoXGudHR+yBEAcl0wf7505m 21 | tNASnulbv5tKaNruz8NH5LECgYASavafw9QSP/qBAT9eqbegUw3D+XxUI9YJPGM0 22 | SPj5D+MEIWxTG3sQsTVUtwBA3/gGQE/WPTDr8c39FwEv3OndkzizjycSvbcfB1ZW 23 | 7m/jyOymb/NsW0WrGJSZE3mitTFupng/twbRoqmBUj8LCBBnGPEVkks/rpDyRj/w 24 | poxBswKBgFcHks/Y9kElBA0hEQL8veANHcLizhmAIr+U+p7NYeQIKpnS6Ie5mad3 25 | n6BiRCIn73TdGg73kFDoHoYVoQ59sRrBM0CGK1YEgarAIbS5epiqGtovzhsg2EWl 26 | AnVL149UG1qx8AkFOyqKDpkX/KdRMP2VRAHcJPIklUJU6PDNpIk4 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/tests/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA44ISMBko1blhaR71EGPuoHjcCC78upL0vS2np4xfsS3LOYwA 3 | xkWxOLu5nRD3CK19q3dIFfDRP8fzDNQSUTH2wJApeYtRQJdgKbeCpMrzXx0+3QQ/ 4 | JV59MNPT+KSlJoAHreey1+HYy9ESECEDDblHVdHCDjzYSLBaGZcm7JlgRivgYQru 5 | kdtU2GUkPA0T2OMpo9BMxlVOlNIwqU9EEl9r8/FxOAHJFTvvG7dsZPG0qNQCOFAF 6 | oBjuL1BMy/oOGZh6jEis06y/0958AdWvN1a+1uC2tv11uJsSgZVumS0Pi7xVRWG1 7 | +ZMm5Nu76d/OuZA52pUhGcj3MB0JpfbQdW+yuwIDAQABAoIBAQC3XX11QMH1yC0n 8 | wai45iJNOjv1aJGPeqRA2Uw4MV5q+kLChgGVzFPzKvKGEkdYuiQAj+oMrQSpyGkv 9 | nLmKsZ90Qnz7FnuNeLQry1lDosOWlIa3MhYG82opcYF19JWbOzN1SbqOXtwSIdbS 10 | IxlH9V32YFJskUOtCl4EJBxeM5wx0OOnjCwyeEvAncqCZnfDyGJGT1yDXGnQytHt 11 | Gy4Di5DfLOx/I+hVTH88fD3w5jluGZRV+NBJJ//DG+fqFArHEYufHg6S1G4MFaYy 12 | jZz8GiyKVzOFNDEuy7wj7dQnKJURCGhz2tG+X5kOkeP7MBKODPnMxb+QcsFvvFxC 13 | RG2rAbEBAoGBAP93WiSAabEBHxEkog2j7W2QerPZECX5iknHMKiTv9oHbyPNKTQ2 14 | nFfCjyY2HFkDV+TJnkNScxSEYiSnyubwemvWqGakwdVX679TXFlOpt3fgm/8VlPu 15 | v/cRha53Uyk0DXnHoqjp1dUZiFcAf4LTsEue4Vxhm4QS1G0dENjmAH07AoGBAOP7 16 | w6THzE7t/eOr/Uho3aWh8jIBtpnjlWUVSL4nV/07lOF/ryuU/s3X0dzhkGAW0k8S 17 | sQLptKMzmZgNo2JBEAwtJgkpK2tcgBchwU3p2rHrEuTDgQqAN7rkH0dLB1+59Y+7 18 | vzo5we0vCgFEGnuqPrXpn4SUQ1PchSPgbbvkaEiBAoGBAIC1pmH7nMSEVx2xAkCz 19 | Fb187IVOWJd5aVYQmJBmmGOGGVXFWPwog46nxK2w14l1aMQpXKZ4lOiCZlwnec/u 20 | 2w8YAJJucgZGHM2xdza7rNDeen5neSsif+9AEcU781cwFZYEogxOe+C403taEeRd 21 | OVZwPwTnXI4nWoV6/nD5OMffAoGALa6pCT+vxLETiqdP3U6F+0Z0DWkiebuMl/Cn 22 | 3tJrYyapMnYvV4BHfl+cgbIBCAAKrCWGqprBw2H3iOxSrMF0wbvaP0Osm5qxX/E3 23 | lrEhTT1ZT9WM6dm/UQec7OAv3hKZDfAE5Vnlbe1bB93poJYActsZR6udwhQR6Zpy 24 | 4o9mBgECgYBJPzPtsUDxlcFAF/EkFwlkX4FLiU5LGtu6PJkkY+05um+hN4G0YCgH 25 | 0kfMCy5qi07ElyrlALwLrL1XlHzwkADH3XhxNL9w217U+n5WeWqks1bdh/VnlW8r 26 | WDnnNPN/5olS3wXtnwTLeFyViHvcYA5kNybIIOpXHmgEsr+vsUzG8A== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /python/pqauth/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from pqauth import crypto 4 | 5 | 6 | class ProtocolError(Exception): 7 | pass 8 | 9 | 10 | class PQAuthClient(object): 11 | def __init__(self, client_key, server_key): 12 | self.client_key = client_key 13 | self.server_key = server_key 14 | 15 | self.server_key_fprint = crypto.public_key_fingerprint(self.server_key) 16 | self.client_key_fprint = crypto.public_key_fingerprint(self.client_key) 17 | 18 | self.client_guid = None 19 | self.server_guid = None 20 | self.expires = None 21 | 22 | 23 | @property 24 | def session_key(self): 25 | return "%s:%s" % (self.client_guid, self.server_guid) 26 | 27 | 28 | def get_hello_message(self): 29 | self.client_guid = crypto.random_guid() 30 | 31 | hello_message = {"client_guid": self.client_guid, 32 | "client_key_fingerprint": self.client_key_fprint} 33 | 34 | return hello_message 35 | 36 | 37 | def process_hello_response(self, response): 38 | # Check the server send back the client_guid we sent. 39 | if response["client_guid"] != self.client_guid: 40 | message = ("Server did not send back the expected client_guid. " 41 | "Expected: %s, Got: %s" % 42 | (self.client_guid, response["client_guid"])) 43 | raise ProtocolError(message) 44 | 45 | # Check the server's stated fingerprint matches the one we know 46 | if response["server_key_fingerprint"] != self.server_key_fprint: 47 | message = ("Server did not send back the expected key fingerprint. " 48 | "Expected: %s, Got: %s" % 49 | (self.server_key_fprint, 50 | response["server_key_fingerprint"])) 51 | raise ProtocolError(message) 52 | 53 | self.expires = response["expires"] 54 | self.server_guid = response["server_guid"] 55 | 56 | 57 | def get_confirmation_message(self): 58 | confirm_message = {"server_guid": self.server_guid} 59 | return confirm_message 60 | 61 | 62 | def encrypt_for_server(self, message): 63 | as_json = json.dumps(message) 64 | return crypto.rsa_encrypt(as_json, self.server_key) 65 | 66 | 67 | def decrypt_from_server(self, encrypted): 68 | decrypted = crypto.rsa_decrypt(encrypted, self.client_key) 69 | return json.loads(decrypted) 70 | 71 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/tests/protocol.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.core.urlresolvers import reverse 3 | from django.conf import settings 4 | 5 | from pqauth import crypto 6 | from pqauth.client import PQAuthClient 7 | from pqauth.pqauth_django_server.views import hello 8 | from pqauth.pqauth_django_server.views import confirm 9 | from pqauth.pqauth_django_server.keys import SERVER_KEY 10 | from pqauth.pqauth_django_server.models import PQAuthSession 11 | 12 | CLIENT_KEY = crypto.load_key_file(settings.TEST_CLIENT_KEY) 13 | EVIL_KEY = crypto.load_key_file(settings.TEST_EVIL_KEY) 14 | 15 | def get_pqa_client(): 16 | return PQAuthClient(CLIENT_KEY, SERVER_KEY) 17 | 18 | def get_evil_pqa_client(): 19 | return PQAuthClient(EVIL_KEY, SERVER_KEY) 20 | 21 | 22 | class ProtocolTest(TestCase): 23 | fixtures = ["test_accounts.json"] 24 | 25 | def post_hello(self, pqa_client): 26 | plaintext_message = pqa_client.get_hello_message() 27 | client_hello = pqa_client.encrypt_for_server(plaintext_message) 28 | response = self.client.generic("POST", reverse(hello), data=client_hello) 29 | 30 | return response 31 | 32 | def test_sunshine_and_unicorns(self): 33 | pqa_client = get_pqa_client() 34 | hello_resp = self.post_hello(pqa_client) 35 | 36 | self.assertEquals(200, hello_resp.status_code) 37 | 38 | decrypted_hello_resp = pqa_client.decrypt_from_server(hello_resp.content) 39 | pqa_client.process_hello_response(decrypted_hello_resp) 40 | 41 | self.assertEquals(pqa_client.client_guid, decrypted_hello_resp["client_guid"]) 42 | self.assertIsNone(decrypted_hello_resp["expires"]) 43 | 44 | confirm_msg = pqa_client.encrypt_for_server(pqa_client.get_confirmation_message()) 45 | confirm_resp = self.client.generic("POST", reverse(confirm), data=confirm_msg) 46 | self.assertEquals(200, confirm_resp.status_code) 47 | 48 | session = PQAuthSession.objects.get(session_key=pqa_client.session_key) 49 | self.assertIsNotNone(PQAuthSession) 50 | 51 | 52 | def test_unknown_client(self): 53 | # Unknown client 54 | # Server's all like "I have no memory of this place" 55 | 56 | evil_client = get_evil_pqa_client() 57 | hello_resp = self.post_hello(evil_client) 58 | 59 | self.assertEquals(403, hello_resp.status_code) 60 | 61 | 62 | def test_mystery_confirmation_guid(self): 63 | # Confirmation server_guid not in the DB 64 | pqa_client = get_pqa_client() 65 | unknown_confirmation = {"server_guid": crypto.random_guid()} 66 | encrypted = pqa_client.encrypt_for_server(unknown_confirmation) 67 | 68 | confirm_resp = self.client.generic("POST", reverse(confirm), data=encrypted) 69 | self.assertEquals(200, confirm_resp.status_code) 70 | 71 | n_sessions = PQAuthSession.objects.count() 72 | self.assertEquals(0, n_sessions) 73 | 74 | 75 | def test_bad_encryption(self): 76 | # Encrypted with a different pubkey 77 | # Client doesn't know where he is and is confused as hell 78 | 79 | stoned_client = PQAuthClient(CLIENT_KEY, EVIL_KEY) 80 | hello_resp = self.post_hello(stoned_client) 81 | 82 | self.assertEquals(400, hello_resp.status_code) 83 | -------------------------------------------------------------------------------- /python/pqauth/pqauth_django_server/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.http import HttpResponse 4 | from django.http import HttpResponseBadRequest 5 | from django.http import HttpResponseForbidden 6 | from django.views.decorators.http import require_POST 7 | from django.views.decorators.http import require_safe 8 | from django.views.decorators.csrf import csrf_exempt 9 | 10 | from pqauth.crypto import rsa_decrypt 11 | from pqauth.crypto import rsa_encrypt 12 | from pqauth.crypto import random_guid 13 | 14 | from pqauth.pqauth_django_server.keys import SERVER_KEY 15 | from pqauth.pqauth_django_server.keys import SERVER_KEY_FINGERPRINT 16 | from pqauth.pqauth_django_server.models import PublicKey 17 | from pqauth.pqauth_django_server.models import PQAuthSession 18 | 19 | 20 | def encrypted_json_post(view_func): 21 | def inner(request, *args, **kwargs): 22 | try: 23 | decrypted_body = rsa_decrypt(request.body, SERVER_KEY) 24 | request.decrypted_json = json.loads(decrypted_body) 25 | return view_func(request, *args, **kwargs) 26 | except ValueError: 27 | msg = ("This endpoint expects a JSON object, " 28 | "encrypted with the server's public RSA key") 29 | return HttpResponseBadRequest(msg) 30 | return csrf_exempt(require_POST(inner)) 31 | 32 | 33 | @require_safe 34 | def public_key(_): 35 | # This only exports the public part of the key 36 | key_text = SERVER_KEY.exportKey(format="OpenSSH") 37 | return HttpResponse(key_text, mimetype="text/plain") 38 | 39 | 40 | @encrypted_json_post 41 | def hello(request): 42 | client_hello = request.decrypted_json 43 | try: 44 | client_key = PublicKey.objects.get( 45 | fingerprint=client_hello["client_key_fingerprint"]) 46 | except PublicKey.DoesNotExist: 47 | return HttpResponseForbidden("Unknown client: %s" % 48 | client_hello["client_key_fingerprint"]) 49 | 50 | response = {"client_guid": client_hello["client_guid"], 51 | "server_guid": random_guid(), 52 | "expires": None, 53 | "server_key_fingerprint": SERVER_KEY_FINGERPRINT} 54 | 55 | started_session = PQAuthSession(server_guid=response["server_guid"], 56 | client_guid=response["client_guid"], 57 | user=client_key.user) 58 | started_session.save() 59 | 60 | encrypted_response = rsa_encrypt(json.dumps(response), 61 | client_key.public_key) 62 | 63 | return HttpResponse(encrypted_response, 64 | mimetype="application/pqauth-encrypted") 65 | 66 | 67 | @encrypted_json_post 68 | def confirm(request): 69 | confirm = request.decrypted_json 70 | guid = confirm["server_guid"] 71 | 72 | try: 73 | started_session = PQAuthSession.objects.get(server_guid=guid) 74 | started_session.session_key = "%s:%s" % (started_session.client_guid, 75 | started_session.server_guid) 76 | started_session.save() 77 | except PQAuthSession.DoesNotExist: 78 | pass 79 | 80 | # It's important to return HTTP 200 here whether or no the confirmation 81 | # succeeded. If you return an error on an unrecognized server_guid, it 82 | # could help an attacker brute-force the session keys 83 | # (by notifying them that they've got half of it) 84 | 85 | return HttpResponse() 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pqAuth 2 | ====== 3 | 4 | Web API Authentication with SSH Public Keys 5 | 6 | 7 | ## The Basics in Python 8 | 9 | ```python 10 | # On the client 11 | 12 | from pqauth import crypto 13 | from pqauth.client import ClientAuthenticator 14 | 15 | # Load the RSA keys, optionally decrypting them if they are encrypted 16 | client_key = crypto.load_key_file("/path/to/id_rsa", password="bosco") 17 | server_public_key = crypto.load_key_file("/path/to/server/id_rsa.pub") 18 | 19 | # paAuth endpoints on the API server 20 | server_hello_url = "https://api.example.com/pqauth/hello" 21 | server_confirm_url = "https://api.example.com/pqauth/confirm" 22 | 23 | authenticator = ClientAuthenticator(client_key, server_public_key, 24 | server_hello_url, server_confirm_url) 25 | 26 | # A string to use as the auth token for further requests 27 | # The server knows the same session_key. 28 | session_key = authenticator.authenticate() 29 | 30 | ``` 31 | 32 | I'm still working on the server-side part, but look at the Django example. 33 | 34 | ## Protocol Overview 35 | 36 | pqAuth is an implementation of the [Needham-Schroeder-Lowe Public-Key Protocol](http://en.wikipedia.org/wiki/Needham%E2%80%93Schroeder_protocol) over HTTP. Using pqAuth, Web APIs and their clients can authenticate eachother using SSH keys, and agree on a *session key*, a temporary authentication token that the client sends along with API requests. 37 | 38 | A pqAuth authentication handshake has four steps: 39 | 40 | ### 1. Client sends random GUID to the server 41 | ```javascript 42 | // encrypted with server's public key 43 | { 44 | client_guid: "6304fb3e-68ed-4e59-bfd5-ab03ebc15762", 45 | client_key_fingerprint: "df:ab:ec:d1:66:ef:32:df:ab:62:d3:4a:0d:f3:f4:28" 46 | } 47 | ``` 48 | 49 | 50 | ### 2. Server sends a random GUID back to the client 51 | ```javascript 52 | // encrypted with client's public key 53 | { 54 | client_guid: "6304fb3e-68ed-4e59-bfd5-ab03ebc15762", 55 | server_guid: "097e21da-2aa9-40d8-9872-8c9698f91e9c", 56 | expires: 1366761788, // optional, session key timeout timestamp 57 | server_key_fingerprint: "46:2b:54:17:2a:28:d0:55:57:2e:68:37:35:b3:6d:a7" 58 | } 59 | ``` 60 | 61 | ### 3. Client sends the server GUID back to the server 62 | ```javascript 63 | // encrypted with server's public key 64 | { 65 | server_guid: "097e21da-2aa9-40d8-9872-8c9698f91e9c", 66 | } 67 | ``` 68 | 69 | ### 4. Client and server create the session key 70 | 71 | ```javascript 72 | // string-concatenate the GUIDs 73 | // this is your client credential 74 | session_key = client_guid + ":" + server_guid 75 | ``` 76 | 77 | 78 | ### Things that are Good to Know 79 | 80 | - The client must include `session_key` in every subsequent API call, but **how** that's done is implementation-specific. (URL parameter, HTTP header, part of an HMAC signature, whatever) 81 | - If the server specified `expires`, the client and server need to do this dance again when the `session_key` expires. 82 | - **Do everything over HTTPS**, if you're not already. While it's safe to do this authentication dance over an insecure channel like HTTP, the `session_key` is a secret, and probably isn't protected in-transit after this authentication dance. But I'm just a developer, I'm not your Dad. Do whatever you want. 83 | 84 | 85 | ## Comparison to SSL Client Authentication 86 | 87 | *"But isn't this what SSL Client Certificates do? And pqAuth doesn't even use a certificate authority!"* 88 | 89 | 90 | Yes, this is sort of what SSL client certificates do, but there are e pretty serious problems with SSL client certificates: 91 | 92 | - **They are nearly impossible to use.** Seriously. Try it some time, it will make you want to stab yourself in the eyeball with a soldering iron. And then, get your load balancer or HTTP server to pass the client identity information along to the app. It's brutal. 93 | - **You can't extract a session key.** After the SSL/TLS negotiation is done, the client and server share a secret, but this is at the transport layer, and you can't really get at it from the application layer. 94 | - **You need a certificate authority to sign client certificates.** On its face, this isn't a bad thing, after all, if Verisign says your client is who he says he is, believe it, right? But good luck getting client certs actually signed by CAs. Sure, you could start up your own CA to sign your client certs, but at that point, you're just using a CA to satisfy SSL's bureaucracy instead of as an actual source of trust. 95 | 96 | SSL client authentication is well intentioned, but the implementation is a disaster. 97 | --------------------------------------------------------------------------------