FreedomBox/plinth/modules/sso/privileged.py
Sunil Mohan Adapa b64ea720fc
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>
2024-12-14 23:41:13 +05:30

105 lines
3.6 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Generate a auth_pubtkt ticket.
Sign tickets with the FreedomBox server's private key.
"""
import base64
import datetime
import os
import pathlib
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from plinth.actions import privileged
KEYS_DIRECTORY = '/etc/apache2/auth-pubtkt-keys'
@privileged
def create_key_pair():
"""Create public/private key pair for signing the tickets."""
keys_directory = pathlib.Path(KEYS_DIRECTORY)
private_key_file = keys_directory / 'privkey.pem'
public_key_file = keys_directory / 'pubkey.pem'
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
private_key = rsa.generate_private_key(public_exponent=65537,
key_size=4096)
def opener(path, flags):
return os.open(path, flags, mode=0o440)
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)
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(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 = [
f'uid={uid}',
f'validuntil={int(validuntil)}',
ip and f'cip={ip}',
tokens and f'tokens={tokens}',
graceperiod and f'graceperiod={int(graceperiod)}',
udata and f'udata={udata}',
extra_fields
and ';'.join(['{}={}'.format(k, v) for k, v in extra_fields]),
]
data = ';'.join(filter(None, fields))
signature = 'sig={}'.format(_sign(private_key, data))
return ';'.join([data, signature])
def _sign(private_key, data):
"""Calculate and return ticket's signature."""
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, '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(private_key, uid, valid_until, tokens=tokens,
graceperiod=grace_period)
def _minutes_from_now(minutes):
"""Return a timestamp at the given number of minutes from now."""
return _seconds_from_now(minutes * 60)
def _seconds_from_now(seconds):
"""Return a timestamp at the given number of seconds from now."""
return (datetime.datetime.now() +
datetime.timedelta(0, seconds)).timestamp()