mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
sso: Switch using cryptography module instead of OpenSSL.crypto
Closes: Debian bug #1088760. - OpenSSL.crypto.sign has been deprecated and in the current version of python3-openssl in Debian testing, it has been dropped. The recommended alternative is cryptography.hazmat.primitives. So, use this instead. - The entire OpenSSL.crypto module is planned to be deprecated in the future. So, stop using it entirely by using cryptography.hazmat.primitives. - sso app does not use openssl anymore, so drop dependency on it. Other apps such as Let's Encrypt do depend on it and but they have their own dependency declared. The freedombox package on the overall retains on 'openssl' package. - We are not using the python OpenSSL module anywhere else, so drop dependency on it. - Use pathlib to simplify some code. - Ensure proper permissions on private and public keys as they are being written to. Tests: - Freshly setup container and ensure that first run succeeds. Permission on the public/private key files and the parent directly are correct. Users are able login to FreedomBox. SSO works when accessing apps such as transmission. - Without patches, setup freedombox container. Apply patches. Permission for keys directory is updated but keys are not overwritten. Login to FreedomBox works. SSO works when accessing apps such as transmission. - Run code to perform signatures using old code and ensure that newer code generates bit-identical signatures. - Running ./run --list-dependencies show 'openssl' and python3-cryptography. - Running unit tests works. - Building debian package works. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net>
This commit is contained in:
parent
235a3cd139
commit
b64ea720fc
2
debian/control
vendored
2
debian/control
vendored
@ -26,6 +26,7 @@ Build-Depends:
|
||||
python3-build,
|
||||
python3-cherrypy3,
|
||||
python3-configobj,
|
||||
python3-cryptography,
|
||||
python3-dbus,
|
||||
python3-django (>= 1.11),
|
||||
python3-django-axes (>= 5.0.0),
|
||||
@ -37,7 +38,6 @@ Build-Depends:
|
||||
python3-gi,
|
||||
python3-markupsafe,
|
||||
python3-mypy,
|
||||
python3-openssl,
|
||||
python3-pampy,
|
||||
python3-paramiko,
|
||||
python3-pexpect,
|
||||
|
||||
2
debian/tests/control
vendored
2
debian/tests/control
vendored
@ -14,5 +14,5 @@ Restrictions: needs-root, breaks-testbed
|
||||
# Run unit and integration tests on installed files.
|
||||
#
|
||||
Test-Command: PYTHONPATH='/usr/lib/python3/dist-packages/' py.test-3 -p no:cacheprovider --cov=plinth --cov-report=html:debci/htmlcov --cov-report=term
|
||||
Depends: git, python3-openssl, python3-pytest, python3-pytest-cov, python3-pytest-django, python3-tomli | python3-coverage (<< 6.0), @
|
||||
Depends: git, python3-pytest, python3-pytest-cov, python3-pytest-django, python3-tomli | python3-coverage (<< 6.0), @
|
||||
Restrictions: breaks-testbed
|
||||
|
||||
@ -206,10 +206,10 @@ autodoc_mock_imports = [
|
||||
'captcha',
|
||||
'cherrypy',
|
||||
'configobj',
|
||||
'cryptography',
|
||||
'dbus',
|
||||
'gi',
|
||||
'markupsafe',
|
||||
'OpenSSL',
|
||||
'pam',
|
||||
'paramiko',
|
||||
'psutil',
|
||||
|
||||
@ -15,7 +15,7 @@ class SSOApp(app_module.App):
|
||||
|
||||
app_id = 'sso'
|
||||
|
||||
_version = 2
|
||||
_version = 3
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Create components for the app."""
|
||||
@ -27,9 +27,9 @@ class SSOApp(app_module.App):
|
||||
'apache'], name=_('Single Sign On'))
|
||||
self.add(info)
|
||||
|
||||
packages = Packages('packages-sso', [
|
||||
'libapache2-mod-auth-pubtkt', 'openssl', 'python3-openssl', 'flite'
|
||||
])
|
||||
packages = Packages(
|
||||
'packages-sso',
|
||||
['libapache2-mod-auth-pubtkt', 'python3-cryptography', 'flite'])
|
||||
self.add(packages)
|
||||
|
||||
dropin_configs = DropinConfigs('dropin-configs-sso', [
|
||||
|
||||
@ -7,8 +7,10 @@ Sign tickets with the FreedomBox server's private key.
|
||||
import base64
|
||||
import datetime
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from OpenSSL import crypto
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
||||
|
||||
from plinth.actions import privileged
|
||||
|
||||
@ -18,33 +20,42 @@ KEYS_DIRECTORY = '/etc/apache2/auth-pubtkt-keys'
|
||||
@privileged
|
||||
def create_key_pair():
|
||||
"""Create public/private key pair for signing the tickets."""
|
||||
private_key_file = os.path.join(KEYS_DIRECTORY, 'privkey.pem')
|
||||
public_key_file = os.path.join(KEYS_DIRECTORY, 'pubkey.pem')
|
||||
keys_directory = pathlib.Path(KEYS_DIRECTORY)
|
||||
private_key_file = keys_directory / 'privkey.pem'
|
||||
public_key_file = keys_directory / 'pubkey.pem'
|
||||
|
||||
os.path.exists(KEYS_DIRECTORY) or os.mkdir(KEYS_DIRECTORY)
|
||||
keys_directory.mkdir(exist_ok=True)
|
||||
# Set explicitly in case permissions are incorrect
|
||||
keys_directory.chmod(0o750)
|
||||
if private_key_file.exists() and public_key_file.exists():
|
||||
# Set explicitly in case permissions are incorrect
|
||||
public_key_file.chmod(0o440)
|
||||
private_key_file.chmod(0o440)
|
||||
return
|
||||
|
||||
if not all([
|
||||
os.path.exists(key_file)
|
||||
for key_file in [public_key_file, private_key_file]
|
||||
]):
|
||||
pkey = crypto.PKey()
|
||||
pkey.generate_key(crypto.TYPE_RSA, 4096)
|
||||
private_key = rsa.generate_private_key(public_exponent=65537,
|
||||
key_size=4096)
|
||||
|
||||
with open(private_key_file, 'w', encoding='utf-8') as priv_key_file:
|
||||
priv_key = crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
||||
pkey).decode()
|
||||
priv_key_file.write(priv_key)
|
||||
def opener(path, flags):
|
||||
return os.open(path, flags, mode=0o440)
|
||||
|
||||
with open(public_key_file, 'w', encoding='utf-8') as pub_key_file:
|
||||
pub_key = crypto.dump_publickey(crypto.FILETYPE_PEM, pkey).decode()
|
||||
pub_key_file.write(pub_key)
|
||||
with open(private_key_file, 'wb', opener=opener) as file_handle:
|
||||
pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption())
|
||||
file_handle.write(pem)
|
||||
|
||||
for fil in [public_key_file, private_key_file]:
|
||||
os.chmod(fil, 0o440)
|
||||
with open(public_key_file, 'wb', opener=opener) as file_handle:
|
||||
public_key = private_key.public_key()
|
||||
pem = public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo)
|
||||
file_handle.write(pem)
|
||||
|
||||
|
||||
def _create_ticket(pkey, uid, validuntil, ip=None, tokens=None, udata=None,
|
||||
graceperiod=None, extra_fields=None):
|
||||
def _create_ticket(private_key, uid, validuntil, ip=None, tokens=None,
|
||||
udata=None, graceperiod=None, extra_fields=None):
|
||||
"""Create and return a signed mod_auth_pubtkt ticket."""
|
||||
tokens = ','.join(tokens)
|
||||
fields = [
|
||||
@ -58,24 +69,27 @@ def _create_ticket(pkey, uid, validuntil, ip=None, tokens=None, udata=None,
|
||||
and ';'.join(['{}={}'.format(k, v) for k, v in extra_fields]),
|
||||
]
|
||||
data = ';'.join(filter(None, fields))
|
||||
signature = 'sig={}'.format(_sign(pkey, data))
|
||||
signature = 'sig={}'.format(_sign(private_key, data))
|
||||
return ';'.join([data, signature])
|
||||
|
||||
|
||||
def _sign(pkey, data):
|
||||
def _sign(private_key, data):
|
||||
"""Calculate and return ticket's signature."""
|
||||
sig = crypto.sign(pkey, data.encode(), 'sha512')
|
||||
return base64.b64encode(sig).decode()
|
||||
signature = private_key.sign(data.encode(), padding.PKCS1v15(),
|
||||
hashes.SHA512())
|
||||
return base64.b64encode(signature).decode()
|
||||
|
||||
|
||||
@privileged
|
||||
def generate_ticket(uid: str, private_key_file: str, tokens: list[str]) -> str:
|
||||
"""Generate a mod_auth_pubtkt ticket using login credentials."""
|
||||
with open(private_key_file, 'r', encoding='utf-8') as fil:
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, fil.read().encode())
|
||||
with open(private_key_file, 'rb') as fil:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
fil.read(), password=None)
|
||||
|
||||
valid_until = _minutes_from_now(12 * 60)
|
||||
grace_period = _minutes_from_now(11 * 60)
|
||||
return _create_ticket(pkey, uid, valid_until, tokens=tokens,
|
||||
return _create_ticket(private_key, uid, valid_until, tokens=tokens,
|
||||
graceperiod=grace_period)
|
||||
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ Test module for sso module operations.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
@ -24,6 +25,10 @@ def fixture_keys_directory(tmpdir):
|
||||
def fixture_existing_key_pair():
|
||||
"""A fixture to create key pair if needed."""
|
||||
privileged.create_key_pair()
|
||||
keys_directory = pathlib.Path(privileged.KEYS_DIRECTORY)
|
||||
assert keys_directory.stat().st_mode == 0o40750
|
||||
assert (keys_directory / 'privkey.pem').stat().st_mode == 0o100440
|
||||
assert (keys_directory / 'pubkey.pem').stat().st_mode == 0o100440
|
||||
|
||||
|
||||
def test_generate_ticket(existing_key_pair):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user