Files
workshop/exercises/tfhers_interoperability/AuthenticationWithDecisionTree.ipynb

19 KiB
Raw Permalink Blame History

Introduction:

Torus ML (CML) supports also model compilation to the TFHE-rs format, enabling deeper interoperability between CML and TFHE-rs frameworks. This allows developers to combine CML’s intuitive, Python-based workflow with the performance and low-level control of TFHE-rs for advanced privacy-preserving applications.

This notebook showcases a privacy-preserving server-side authentication flow, where access to a remote server is granted only if the user’s encrypted information satisfies a specific condition — all without ever decrypting any sensitive data throughout the process.

On the server side, verification is performed using a binary classification algorithm provided by CML. The post-processing part, which includes an argmax operation on the model’s encrypted output followed by a multiplication with a random plaintext token (known only to the server), is implemented using the TFHE-rs library.

This token acts as a proof of successful authentication and is returned only if the condition is met.

In [1]:
import subprocess
from tempfile import TemporaryDirectory

import torus_ml_extensions as fhext
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

from torus.ml.common.utils import CiphertextFormat
from torus.ml.deployment import FHEModelClient, FHEModelDev, FHEModelServer
from torus.ml.sklearn import DecisionTreeClassifier as TorusDecisionTreeClassifier
No CUDA runtime is found, using CUDA_HOME='/usr/local/cuda'
In [2]:
# Generate toy classification dataset
x, y = make_classification(n_samples=100, class_sep=2, n_features=10, random_state=42)

X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)
In [3]:
# For now, only 8-bit quantization is supported for TFHE-rs interoperability
model = TorusDecisionTreeClassifier(n_bits=8)
# Train the CML model on clear data
model.fit(X_train, y_train)
Out [3]:
DecisionTreeClassifier(n_bits={'op_inputs': 8, 'op_leaves': 8})
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
DecisionTreeClassifier(n_bits={'op_inputs': 8, 'op_leaves': 8})
In [4]:
# Compile the model with TFHE-rs support (ciphertext_format=CiphertextFormat.TFHE_RS)
model.compile(X_train, ciphertext_format=CiphertextFormat.TFHE_RS);
In [5]:
# Use a temporary directory to store the Dev, client, and server models
with TemporaryDirectory() as temp_dir:
    # Export the model for deployment using the FHEModelDev API
    model_dev = FHEModelDev(temp_dir, model)
    model_dev.save()

    # Loading the model on the client side using FHEModelClient API
    fhe_model_client = FHEModelClient(path_dir=temp_dir)
    fhe_model_client.load()

    # Loading the model on the server side using FHEModelServer API
    fhe_model_server = FHEModelServer(temp_dir)
    fhe_model_server.load()
In [6]:
# Client side : Generates all keys and serializes the evaluation keys required for the server
evaluation_keys, tfhers_evaluation_keys = fhe_model_client.get_serialized_evaluation_keys(
    include_tfhers_key=True
)

with open("evalkeys_tfhers.bin", "wb") as fp:
    fp.write(tfhers_evaluation_keys)

Use Case: Privacy-Preserving Authentication with Homomorphic Encryption

In this scenario, the client attempts to authenticate using sensitive data (e.g., a biometric vector such as facial recognition).

On the client side, the input is first quantized (to meet FHE constraints), then encrypted, and finally sent to the server.

On the server side, first, a binary classification algorithm — in this case, a decision tree — is applied on the encrypted data to determine whether authentication should succeed (decision = 1) or fail (decision = 0). Then, the decision result is homomorphically multiplied by a secret authentication token known only to the server. Finally, the encrypted result is sent back to the client.

On the client side, the result is decrypted using the secret key. If the authentication was successful (decision = 1), the decrypted value represents the original server token. Otherwise (decision = 0), the decrypted result is a vector of zeros, indicating authentication failure.

In [7]:
for DATA_INDEX in range(10):

    print(f"🔢 Test sample #{DATA_INDEX}", "=" * 50)
    print("Inference input:", X_test[[DATA_INDEX]])
    print(f"🎯 Ground truth label: {y_test[DATA_INDEX]}")

    # Client side : Quantize, encrypt and serialize the data
    q_x_encrypted_serialized = fhe_model_client.quantize_encrypt_serialize(X_test[[DATA_INDEX]])

    # Server side: Run the model over encrypted data
    q_y_pred_encrypted_serialized = fhe_model_server.run(q_x_encrypted_serialized, evaluation_keys)

    # Client side : Decrypt, de-quantize and post-process the result
    y_pred = fhe_model_client.deserialize_decrypt_dequantize(q_y_pred_encrypted_serialized[0])

    print("Model logits (after decryption):", y_pred.reshape((-1,)))

    with open("prediction_non_preprocessed.bin", "wb") as f:
        f.write(q_y_pred_encrypted_serialized[0])

    # ***************************************
    # NOW RUN cargo run --release
    # **************************************

    subprocess.run(
        [
            "cargo",
            "run",
            "--release",
            "--",  # Required to pass args to the Rust binary
            "evalkeys_tfhers.bin",
            "prediction_non_preprocessed.bin",
            "tfhers_sign_result.bin",
        ],
        capture_output=True,
        text=True,
        check=True,
    )

    with open("tfhers_sign_result.bin", "rb") as f:
        tfhers_postproc_bytes = f.read()

    results = fhext.decrypt_radix(
        blob=tfhers_postproc_bytes,
        shape=(1, 16),
        bitwidth=8,
        is_signed=True,
        secret_key=fhe_model_client.get_tfhers_secret_key(),
    )

    token = results.reshape((-1,))

    if all(t == 0 for t in token):
        print(f"🔐 Authentication token: 🛑 Access denied — token: {token}\n")
    else:
        print(f"🔐 Authentication token: 🟢 Access granted — token: {token} \n")
🔢 Test sample #0 ==================================================
Inference input: [[-0.07015438  2.85605469 -1.05319823  2.4898248  -0.21910053  0.32692737
  -2.21113531  0.77086519  0.82940558  0.23561456]]
🎯 Ground truth label: 0
Model logits (after decryption): [1. 0.]
🔐 Authentication token: 🛑 Access denied — token: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

🔢 Test sample #1 ==================================================
Inference input: [[-2.43430912  2.25291095  2.09885263  1.23502851 -0.24751864 -0.72713718
   0.6206721  -1.33534436 -0.07443343  0.177701  ]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [-122  -15  106  -56   85   39  -28   94 -109   10   27  -51  -33   60
  -25   11] 

🔢 Test sample #2 ==================================================
Inference input: [[-3.37679987  3.30450019  2.83991037  1.87087893 -0.37482081 -0.2403254
   0.44426331  1.1593298   0.71095997 -0.36096617]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [ 117  -34 -113   75  -19   55 -125  116   18   94   35  100   -5  -87
   67  -95] 

🔢 Test sample #3 ==================================================
Inference input: [[-1.97994267  1.53479393  1.82584805  0.74282242 -0.47874862  0.22213377
  -0.8946073  -0.43973106  1.25575613 -0.18687164]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [ -20   45  103    3    7  -47  -58   -4 -111   82  -46   52  -56  -10
  -31  -30] 

🔢 Test sample #4 ==================================================
Inference input: [[-1.19229599 -1.71352532  2.15199146 -1.87205691 -0.59939265 -0.83972184
  -0.52575502  0.15039379 -2.12389572 -0.75913266]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [  -9 -112  102   71 -122   16  -54    5  -49   94  -29  -34  -52  -73
  -35  125] 

🔢 Test sample #5 ==================================================
Inference input: [[ 2.48305377 -2.20592943 -2.17762637 -1.17878084  0.23204994 -0.47103831
  -1.40746377 -0.21344715 -1.44808434 -0.71844422]]
🎯 Ground truth label: 0
Model logits (after decryption): [1. 0.]
🔐 Authentication token: 🛑 Access denied — token: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

🔢 Test sample #6 ==================================================
Inference input: [[ 2.85855887 -2.43534875 -2.54851014 -1.26544183  0.40171172 -0.10876015
  -0.40122047  0.0125924   0.69014399  0.22409248]]
🎯 Ground truth label: 0
Model logits (after decryption): [1. 0.]
🔐 Authentication token: 🛑 Access denied — token: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

🔢 Test sample #7 ==================================================
Inference input: [[-2.02146424  2.02703224  1.68057323  1.16292288  0.70030988 -0.85835778
   0.12200981 -0.0960599  -0.57563783  2.56008454]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [-117   97  -78   21    7 -104  106  -89 -104   77  -55   37   64   80
  -41  -96] 

🔢 Test sample #8 ==================================================
Inference input: [[-1.96346177  1.88629356  1.66530077  1.05694552 -1.12905177  0.72167206
   0.48937456  0.71299843 -0.52452027 -1.22212781]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [  85  -83   64 -105 -100 -116  -19  -71   31  -70  108   10   33   46
 -118   93] 

🔢 Test sample #9 ==================================================
Inference input: [[-2.27657182  2.35970566  1.86199147  1.37726866  0.60600995 -1.55662917
   1.75479418  1.69645637 -1.28042935 -2.08192941]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [ -50  -97  -99 -115   -1  -10  -92   46 -115 -111 -115  127  -24   38
  125   92] 

Conclusion

In this notebook, we showed how to combine Torus ML (CML) and TFHE-rs for end-to-end encrypted processing. After training and compiling a CML decision tree, we performed an encrypted inference and then applied additional post-processing in TFHE-rs, all while preserving data privacy.

This approach highlights the power of interoperability between CML and TFHE-rs, and paves the way for more advanced privacy-preserving use cases.

Disclaimer: This use case assumes an honest-but-curious server and does not represent a complete secure authentication protocol. Further measures are required to protect against malicious servers.