Developer Guide Help

Federated Authentication

A core feature of Cafe Variome V3 is its decentralized, zero-trust discovery network. This Wiki page focuses on the complex security measures and authentication principles of the federated network. For an overview of the general authentication model, refer to the Authentication Model page.

Server keys

Each instance of Cafe Variome generates a new identity when joining or creating a network. While the info endpoints still return basic details such as the owner and address, the installation ID and cryptographic keys are newly generated. This enables semi-anonymity and allows for the separation of network permissions.

The keys are generated by the Python backend and immediately stored in the vault. They are standard RSA 4096 keys, stored in (PEM) format within the (KV engine).

private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096) public_key = private_key.public_key() private_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() ) public_pem = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ) self.client.secrets.kv.v2.create_or_update_secret( path=self.kv2_prefix + '/network/' + network_id, secret=dict(private_key=private_pem.decode(), public_key=public_pem.decode()), mount_point=self.kv2_mount_point, )

During a network joining request, the public key is included in the request. If the request is approved, the key is stored in the vault. A notification about the new node joining the network is then propagated to all nodes; otherwise, the new node's information will be synced during the daily network sync. However, each node must individually approve the new node before it is fully recognized. Until approval, the key is stored separately. Only once the key is in place can the new node communicate with this server. This enables one-way communication, where one server may accept another, but the reverse is not required.

Signing the requests

The signature generated with the server key is a SHA256 hash of the request body, signed using the private key. This signature is then included in the request header. The transit engine is not used for server key implementation because the keys need to be exported and imported, a process that the transit engine does not handle efficiently.

private_key = serialization.load_pem_private_key( private_pem.encode(), password=None, backend=default_backend() ) signature = private_key.sign( payload.encode(), padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH ), hashes.SHA256() ) return base64.b64encode(signature).decode()

The encoding and decoding process is implemented to address codec and platform differences. The result is a string that remains readable and can be encoded on any platform.

Access tokens

Access tokens are used to authenticate with Keycloak for both users and service accounts. The process of acquiring and validating an access token is straightforward. However, if two instances use different Keycloak servers or belong to different (realms), the access token cannot be verified directly. To handle this, user remapping is implemented.

User remapping

User remapping is the process of mapping a user to an internal user ID, granting them the permissions associated with that internal user. This mapping does not have to be one-to-one. There are three types of user remapping:

No mapping

This is the simplest case, where the user has no predefined mapping. The user ID, while not present in Keycloak, is extracted directly from the token. If the server is configured to accept these IDs from the database, the operation will still proceed successfully. However, this approach is highly discouraged as it poses security risks.

Static mapping

All users from a single installation are mapped to a single internal user. This is the default behavior when the authentication provider is not trusted. All queries from this node are executed under the privileges of this internal user; however, the original user ID is still stored. This ensures that queries remain encrypted with the original user’s key and allows for query auditing.

One thing worth noting is that if the internal user is granted Range access to any source, this enables differential privacy for that source. However, since all users share the same privacy budget, it may be depleted quickly.

Dynamic mapping

This feature is not implemented yet.

User keys

For a user to log in to an instance, they must have a key in the transit engine. When an admin grants or revokes a user’s login access, they are effectively adding or removing the key from the system. The key is generated within the transit engine and never leaves it, as all encryption and decryption operations are performed directly within the transit engine.

try: self.client.secrets.transit.create_key( name=user_id, key_type='rsa-4096', mount_point=self.transit_mount_point, ) except hvac.exceptions.InvalidRequest as e: # Key already exists return False # Make the key deletable self.client.secrets.transit.update_key_configuration( name=user_id, deletion_allowed=True, mount_point=self.transit_mount_point, )

The transit engine requires keys to be marked as deletable before they can be removed. User keys are not used for signing, as they can be regenerated or deleted at any time without the other instance being aware. Instead, they are used solely for encrypting query responses. To encrypt:

public_key = serialization.load_pem_public_key( public_pem.encode(), backend=default_backend() ) aes_key = os.urandom(32) aes_iv = os.urandom(16) cipher = Cipher(algorithms.AES(aes_key), modes.CBC(aes_iv), backend=default_backend()) encryptor = cipher.encryptor() encrypted_payload = encryptor.update(payload.encode()) + encryptor.finalize() encrypted_aes_key = public_key.encrypt( aes_key, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) return (base64.b64encode(encrypted_aes_key).decode(), base64.b64encode(aes_iv).decode(), base64.b64encode(encrypted_payload).decode())

To decrypt:

decrypt_response = self.client.secrets.transit.decrypt_data( name=user_id, ciphertext=encrypted_key, mount_point=self.transit_mount_point ) aes_key = decrypt_response['data']['plaintext'] aes_key = base64.b64decode(aes_key) cipher = Cipher(algorithms.AES(aes_key), modes.CBC(base64.b64decode(iv)), backend=default_backend()) decryptor = cipher.decryptor() decrypted_payload = decryptor.update(base64.b64decode(encrypted_payload)) + decryptor.finalize() return decrypted_payload.decode()

Because the payload is long, the RSA key is only used to encrypt an AES key, and the payload is encrypted with AES symmetric encryption.

Last modified: 31 March 2025