From 28298d3ce69a86b81a0b3eeae02559b4347183fc Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Fri, 31 Mar 2023 21:41:12 +0200 Subject: [PATCH] replace predefined ssh keys with generated ones --- bundles/backup-client/items.py | 7 ++- bundles/backup-server/items.py | 8 +-- libs/ssh.py | 96 ++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 libs/ssh.py diff --git a/bundles/backup-client/items.py b/bundles/backup-client/items.py index 6538803..a4c9a11 100644 --- a/bundles/backup-client/items.py +++ b/bundles/backup-client/items.py @@ -33,14 +33,17 @@ else: backup_target = repo.get_node(node.metadata.get('backup-client/target')) files['/etc/backup.priv'] = { - 'content': repo.vault.decrypt_file(join('backup', 'keys', f'{node.name}.key.vault')), + 'content': repo.libs.ssh.generate_ed25519_private_key( + node.metadata.get('backup-client/user-name'), + backup_target, + ), 'mode': '0400', } files['/usr/local/bin/generate-backup'] = { 'content_type': 'mako', 'context': { - 'username': node.metadata['backup-client']['user-name'], + 'username': node.metadata.get('backup-client/user-name'), 'server': backup_target.metadata.get('backup-server/my_hostname'), 'port': backup_target.metadata.get('backup-server/my_ssh_port'), 'paths': backup_paths, diff --git a/bundles/backup-server/items.py b/bundles/backup-server/items.py index 11d0624..bd4d12f 100644 --- a/bundles/backup-server/items.py +++ b/bundles/backup-server/items.py @@ -27,9 +27,6 @@ directories['/etc/backup-server/clients'] = { sudoers = {} for nodename, config in node.metadata.get('backup-server/clients', {}).items(): - with open(join(repo.path, 'data', 'backup', 'keys', f'{nodename}.pub'), 'r') as f: - pubkey = f.read().strip() - sudoers[config['user']] = nodename users[config['user']] = { @@ -41,7 +38,10 @@ for nodename, config in node.metadata.get('backup-server/clients', {}).items(): } files[f'/srv/backups/{nodename}/.ssh/authorized_keys'] = { - 'content': pubkey, + 'content': repo.libs.ssh.generate_ed25519_public_key( + config['user'], + node, + ), 'owner': config['user'], 'mode': '0400', 'needs': { diff --git a/libs/ssh.py b/libs/ssh.py new file mode 100644 index 0000000..90fb674 --- /dev/null +++ b/libs/ssh.py @@ -0,0 +1,96 @@ +from base64 import b64decode, b64encode +from functools import lru_cache +from hashlib import sha3_224 + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import ( + Encoding, + NoEncryption, + PrivateFormat, + PublicFormat, +) + +from bundlewrap.utils import Fault + + +@lru_cache(maxsize=None) +def generate_ed25519_private_key(username, node): + return Fault( + f'private key {username}@{node.name}', + lambda username, node: _generate_ed25519_private_key(username, node), + username=username, + node=node, + ) + + +@lru_cache(maxsize=None) +def generate_ed25519_public_key(username, node): + return Fault( + f'public key {username}@{node.name}', + lambda username, node: _generate_ed25519_public_key(username, node), + username=username, + node=node, + ) + + +def _generate_ed25519_private_key(username, node): + privkey_bytes = Ed25519PrivateKey.from_private_bytes(_secret(username, node)) + + nondeterministic_privatekey = privkey_bytes.private_bytes( + encoding=Encoding.PEM, + format=PrivateFormat.OpenSSH, + encryption_algorithm=NoEncryption(), + ).decode() + + # get relevant lines from string + nondeterministic_bytes = b64decode( + ''.join(nondeterministic_privatekey.split('\n')[1:-2]) + ) + + # sanity check + if nondeterministic_bytes[98:102] != nondeterministic_bytes[102:106]: + raise Exception("checksums should be the same: whats going on here?") + + # replace random bytes with deterministic values + random_bytes = sha3_224(_secret(username, node)).digest()[0:4] + deterministic_bytes = ( + nondeterministic_bytes[:98] + + random_bytes + + random_bytes + + nondeterministic_bytes[106:] + ) + + # reassemble file + deterministic_privatekey = '\n'.join( + [ + '-----BEGIN OPENSSH PRIVATE KEY-----', + b64encode(deterministic_bytes).decode(), + '-----END OPENSSH PRIVATE KEY-----', + ] + ) + '\n' + + return deterministic_privatekey + + +def _generate_ed25519_public_key(username, node): + return ( + Ed25519PrivateKey.from_private_bytes(_secret(username, node)) + .public_key() + .public_bytes( + encoding=Encoding.OpenSSH, + format=PublicFormat.OpenSSH, + ) + .decode() + + f' {username}@{node.name}' + ) + + +@lru_cache(maxsize=None) +def _secret(username, node): + return b64decode( + str( + node.repo.vault.random_bytes_as_base64_for( + f"{username}@{node.name}", length=32 + ) + ) + )