A bank’s credit scoring model is not just a few numbers—it is the bank’s credit policy, distilled from years of loan data. A customer’s income, debts and payment history are not just a few numbers either—they are exactly what the customer should not have to hand over to every lender they talk to. So we have a familiar standoff: the model owner wants to keep the model secret, the data owner wants to keep the data secret, and both of them want a prediction. In this post, we’ll break that standoff for the two workhorses of classical machine learning, logistic regression and linear SVM, using partially homomorphic encryption with LightPHE in Python, Go, Java and TypeScript.

The customer encrypts their features, the bank runs its private model on the ciphertexts, and the customer learns only the predicted class—approved or declined. The bank learns nothing about the features, and the customer learns nothing about the model, not even the score. This is encrypted machine learning, and it is based on the classifiers of Bost et al. in Machine Learning Classification over Encrypted Data (NDSS 2015).
🙋♂️ You may consider to enroll my top-rated cryptography course on Udemy
Logistic Regression and Linear SVM Look the Same at Prediction Time
Logistic regression and linear SVM are trained very differently, but once trained, they predict in exactly the same way. Both draw a hyperplane through the feature space and check which side of it a sample falls on—that’s also why logistic regression is a linear model, despite its S-shaped curve. That’s why they are called hyperplane classifiers.
logistic regression: P(approved) = sigmoid(w . x + b) -> approved if w . x + b > 0 linear svm: approved if w . x + b > 0
The sigmoid only squashes the score into a probability, and since it is monotonic, sigmoid(w . x + b) > 0.5 is the same as w . x + b > 0. So we never need to compute a sigmoid under encryption. That’s good news, because homomorphic encryption is great at adding and terrible at exponentials.
With more than two classes, a one-vs-rest linear SVM or a multinomial logistic regression keeps one weight vector per class and picks the class with the highest score. A binary classifier fits the same shape: class 0 gets the weights 0 and bias 0, class 1 gets w and b, and the greater score wins.
score_i = w_i . x + b_i for every class i prediction = argmax_i score_i
So encrypted hyperplane classification boils down to two problems: computing the scores on encrypted features, and finding the greatest encrypted score.
Required Homomorphic Features
1. Additive Homomorphism and Scalar Multiplication: Computing the Scores
The features are encrypted, but the weights are the server’s own plain numbers. A score is just a weighted sum, so the server needs to multiply a ciphertext by a plain constant and to add ciphertexts. Additively homomorphic cryptosystems give us both with the public key alone:
E(score_i) = E(b_i) * E(x_1)^(w_i1) * E(x_2)^(w_i2) * ... * E(x_n)^(w_in)
= E(w_i1 * x_1 + w_i2 * x_2 + ... + w_in * x_n + b_i)
This part needs no interaction at all. The server computes every class score on its own, without the private key.
2. Integers: Quantizing the Model
Homomorphic encryption works on integers, while a trained model has float weights like 0.042. The fix is quantization: multiply weights and biases by a scale such as 1000 and round them, e.g. 0.042 becomes 42. Every score is multiplied by the same scale, so the argmax doesn’t change, apart from rounding errors for samples sitting right on the decision boundary. Features must be integers as well, e.g. income in thousands of dollars.
Scores can be negative, while plaintexts live in a modular space. LightPHE handles this by shifting every score by 2^bit_length, which keeps the argmax and makes them non-negative. You only have to pick a bit_length such that the absolute value of every score is less than 2^bit_length.
3. Encrypted Argmax: Finding the Winner Without Revealing the Scores
Now the server holds encrypted scores and cannot decrypt them. The obvious shortcut is to send the scores to the customer, who decrypts them and picks the greatest. Don’t do that. The scores leak the model. A score is a linear function of the features, so a customer who submits n + 1 well-chosen applications and reads their scores can solve a linear system and recover every weight and the bias. For our 4-feature loan model, five fake applications would give away the bank’s whole credit policy.
So we need the argmax itself under encryption. It walks through the classes, keeping a running maximum and its index, and every step needs two things that additive encryption cannot do alone:
- Comparing two encrypted scores. The server adds a uniformly random mask to the difference of the two scores and sends it to the customer. The customer sees only a random number, encrypts its bits, and sends them back. The server then runs the secure comparison of Damgard-Geisler-Kroigaard on these bits against values derived from its own mask, with randomly flipped comparisons, the same trick we used in encrypted decision trees. The result is an encrypted bit,
E(candidate > max), that neither side can read. - Multiplying two encrypted values. To update the running maximum, the server needs
max += bit * (candidate - max), and the same for the index. That’s a ciphertext times a ciphertext. The server masks both with random values, the customer multiplies the masked values, and the server removes the masks homomorphically.
Every value the customer decrypts along the way is masked with fresh randomness, so the customer learns nothing until the very end, when it receives a re-randomized encryption of the winning index. On ties, the earlier class wins.
4. A Few Round Trips
Encrypted argmax is interactive: each class after the first one costs 3 round trips between the server and the customer, one for the masked difference, one for the comparisons, and one for the masked products. A binary classifier needs 3 round trips, and a 3-class one needs 6. In LightPHE, the server reaches the customer through a callable. Locally, that’s just the customer’s answer_argmax. In production, you wrap your network call in it.
5. A Small Plaintext Space With a Cheap Zero Test
The comparison above has the customer encrypt the masked difference bit by bit, and checks blinded values for zero. Both are only cheap when the plaintext space is small. Damgard-Geisler-Kroigaard is designed for exactly that: its plaintext space is a prime of about 32 bits, a masked difference is about 32 encrypted bits, and checking for zero is a single exponentiation. That’s also where the size limit comes from: scores must fit into about 30 bits, which is plenty for quantized linear models.
Which Algorithms Can Be Used, and Why
LightPHE ships with more than 25 cryptosystems, but only one of them supports encrypted classification: Damgard-Geisler-Kroigaard. If you try another one, e.g. Paillier, LightPHE stops you with does not support encrypted classification. Here’s why the others fall short:
| Algorithms | Encrypted scores | Encrypted argmax | Why |
| Damgard-Geisler-Kroigaard | ✅ | ✅ | Additive with scalar multiplication, plus a small plaintext space, cheap zero test and secure comparison, all designed for this job |
| Paillier, Damgard-Jurik, Exponential ElGamal, EC ElGamal, Okamoto-Uchiyama, Benaloh, Naccache-Stern, Joye-Libert and other additive schemes | ✅ | ❌ | They can compute the scores, but the comparison relies on a small plaintext space. With a 1024-bit plaintext space, each masked difference would become over a thousand encrypted bits, and every zero check a full decryption |
| RSA, ElGamal | ❌ | ❌ | Multiplicatively homomorphic only, so they cannot even add up a weighted sum |
| Goldwasser-Micali, ML-KEM, Sander-Young-Yung | ❌ | ❌ | XOR or AND of bits only, so they cannot multiply features by weights |
Interestingly, Bost et al. combine Paillier for some of the arithmetic with DGK for the comparisons. Once the model is quantized, the scores comfortably fit into DGK’s plaintext space, so LightPHE keeps the whole pipeline in Damgard-Geisler-Kroigaard, with one key pair and no conversions between cryptosystems.
Why Not Encrypt the Weights Too?
A natural question: if we encrypt the features, why not encrypt the weights as well, and multiply ciphertexts with a scheme that supports one multiplication, such as Boneh-Goh-Nissim (BGN)? A dot product needs exactly one multiplication per term, so BGN could indeed compute E(w . x + b) from encrypted weights and encrypted features. It just doesn’t help here, for two reasons.
First, in our setting the weights never leave the bank. The bank runs its own model on its own server, so the customer never sees the weights in the first place, and multiplying a ciphertext by a plain weight is already the cheapest operation we have. Encrypting the weights would add cost without adding privacy. Worse, they would have to be encrypted under the customer’s public key, and the customer holds the private key, so the model would become decryptable by the very party it’s hidden from.
Second, BGN would only solve the easy half. Whichever way the scores are computed, we end up with encrypted scores that must not be decrypted, because decrypting them leaks the model. BGN spends its single multiplication on the dot product, and comparing scores needs far more than one more multiplication, so we would still need an interactive comparison protocol for the argmax—which is exactly what Damgard-Geisler-Kroigaard provides.
Where encrypted weights do make sense is outsourced inference: the bank uploads its model to a cloud provider it doesn’t trust, and customers send their encrypted features to that cloud. Now both operands are ciphertexts, and BGN’s one multiplication fits the dot product perfectly. But that setting raises new problems of its own—if weights and features are encrypted under the same key, its holder can read both, which calls for threshold keys or proxy re-encryption—and it still ends with an encrypted argmax.
| Setting | Weights | Scores need | Then |
| Bank runs its own model (this post) | Plain | Addition and scalar multiplication, e.g. DGK | Encrypted argmax |
| Untrusted cloud runs the bank’s model | Encrypted | One multiplication, e.g. BGN | Key management and encrypted argmax |
Example 1: Loan Approval with Logistic Regression
A customer applies for a loan from a mobile app. The bank’s logistic regression model uses 4 features: income and debt in thousands of dollars, years of employment, and the number of late payments. Its weights tell how much each feature pushes the decision, as explained in feature importance in logistic regression—which is exactly why the bank wants to keep them secret. It was trained on floats, e.g. with scikit-learn, and the bank quantizes it by 1000 right before serving it. The customer has an income of 85K, a debt of 20K, 6 years of employment, and no late payments.
from lightphe import LightPHE # customer builds the cryptosystem, keeps the private key and shares the public key customer_cs = LightPHE(algorithm_name = "Damgard-Geisler-Kroigaard") customer_cs.export_keys(target_file = "public.txt", public = True) # income and debt in thousands, years of employment, late payments features = [85, 20, 6, 0] encrypted_features = [customer_cs.encrypt(x) for x in features] # bank's logistic regression model, trained on floats and quantized by 1000 bank_cs = LightPHE(algorithm_name = "Damgard-Geisler-Kroigaard", key_file = "public.txt") float_weights = [0.042, -0.065, 0.31, -0.9] float_bias = -1.2 # class 0 declines with a score of 0, class 1 approves with a score of w.x + b weights = [[0, 0, 0, 0], [round(w * 1000) for w in float_weights]] biases = [0, round(float_bias * 1000)] # bank computes encrypted scores and finds the greater one with the customer's help. # absolute values of scores must be less than 2^bit_length encrypted_decision = bank_cs.evaluate_hyperplane( encrypted_features, weights, bit_length = 16, client = customer_cs.answer_argmax, biases = biases ) # customer learns only the decision: 1 means approved assert customer_cs.decrypt(encrypted_decision) == 1
package main
import (
"fmt"
"log"
"math"
"github.com/serengil/lightphe-go/lightphe"
)
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
// customer builds the cryptosystem, keeps the private key and shares the public key
customerCs, err := lightphe.New(lightphe.DamgardGeislerKroigaard)
check(err)
check(customerCs.ExportKeys("public.txt", true))
// income and debt in thousands, years of employment, late payments
features := []int64{85, 20, 6, 0}
var encryptedFeatures []*lightphe.Ciphertext
for _, x := range features {
c, err := customerCs.EncryptInt(x)
check(err)
encryptedFeatures = append(encryptedFeatures, c)
}
// bank's logistic regression model, trained on floats and quantized by 1000
bankCs, err := lightphe.New(lightphe.DamgardGeislerKroigaard, lightphe.WithKeyFile("public.txt"))
check(err)
floatWeights := []float64{0.042, -0.065, 0.31, -0.9}
floatBias := -1.2
// class 0 declines with a score of 0, class 1 approves with a score of w.x + b
approve := make([]int64, len(floatWeights))
for i, w := range floatWeights {
approve[i] = int64(math.Round(w * 1000))
}
weights := [][]int64{make([]int64, len(floatWeights)), approve}
biases := []int64{0, int64(math.Round(floatBias * 1000))}
// bank computes encrypted scores and finds the greater one with the customer's help.
// absolute values of scores must be less than 2^bitLength
encryptedDecision, err := bankCs.EvaluateHyperplane(encryptedFeatures, weights, 16, customerCs.AnswerArgmax, biases)
check(err)
// customer learns only the decision: 1 means approved
decision, err := customerCs.Decrypt(encryptedDecision)
check(err)
fmt.Println("approved:", decision.Int64() == 1) // approved: true
}
import io.github.serengil.lightphe.LightPHE;
import io.github.serengil.lightphe.model.Ciphertext;
import java.math.BigInteger;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
// customer builds the cryptosystem, keeps the private key and shares the public key
LightPHE customerCs = LightPHE.builder().algorithmName("Damgard-Geisler-Kroigaard").build();
customerCs.exportKeys(Path.of("public.txt"), true);
// income and debt in thousands, years of employment, late payments
List<Long> features = List.of(85L, 20L, 6L, 0L);
List<Ciphertext> encryptedFeatures = features.stream().map(customerCs::encrypt).toList();
// bank's logistic regression model, trained on floats and quantized by 1000
LightPHE bankCs = LightPHE.builder()
.algorithmName("Damgard-Geisler-Kroigaard")
.keyFile(Path.of("public.txt"))
.build();
double[] floatWeights = {0.042, -0.065, 0.31, -0.9};
double floatBias = -1.2;
// class 0 declines with a score of 0, class 1 approves with a score of w.x + b
List<Long> decline = new ArrayList<>();
List<Long> approve = new ArrayList<>();
for (double w : floatWeights) {
decline.add(0L);
approve.add(Math.round(w * 1000));
}
List<List<Long>> weights = List.of(decline, approve);
List<Long> biases = List.of(0L, Math.round(floatBias * 1000));
// bank computes encrypted scores and finds the greater one with the customer's help.
// absolute values of scores must be less than 2^bitLength
Ciphertext encryptedDecision = bankCs.evaluateHyperplane(
encryptedFeatures, weights, 16, customerCs::answerArgmax, biases);
// customer learns only the decision: 1 means approved
assert customerCs.decrypt(encryptedDecision).equals(BigInteger.ONE);
import { LightPHE } from 'lightphe-ts';
// customer builds the cryptosystem, keeps the private key and shares the public key
const customerCs = new LightPHE({ algorithmName: 'Damgard-Geisler-Kroigaard' });
customerCs.exportKeys('public.txt', true);
// income and debt in thousands, years of employment, late payments
const features = [85, 20, 6, 0];
const encryptedFeatures = features.map((x) => customerCs.encrypt(x));
// bank's logistic regression model, trained on floats and quantized by 1000
const bankCs = new LightPHE({ algorithmName: 'Damgard-Geisler-Kroigaard', keyFile: 'public.txt' });
const floatWeights = [0.042, -0.065, 0.31, -0.9];
const floatBias = -1.2;
// class 0 declines with a score of 0, class 1 approves with a score of w.x + b
const weights = [floatWeights.map(() => 0), floatWeights.map((w) => Math.round(w * 1000))];
const biases = [0, Math.round(floatBias * 1000)];
// bank computes encrypted scores and finds the greater one with the customer's help.
// absolute values of scores must be less than 2^bitLength
const encryptedDecision = await bankCs.evaluateHyperplane(
encryptedFeatures,
weights,
16,
customerCs.answerArgmax,
{ biases }
);
// customer learns only the decision: 1 means approved
console.assert(customerCs.decrypt(encryptedDecision) === 1n);
In plain floats, this customer’s score is 0.042 * 85 - 0.065 * 20 + 0.31 * 6 - 0.9 * 0 - 1.2 = 2.93, which is positive, so the loan is approved, and the encrypted pipeline agrees. An applicant with an income of 30K, a debt of 45K, 1 year of employment and 3 late payments scores -5.255 and is declined. Either way, the customer learns the decision only: not the score, not how close the decision was, and not the weights.
Example 2: Email Routing with a Multi-Class Linear SVM
An email client asks a classification service whether an incoming email belongs in primary, promotions or spam. The service has a one-vs-rest linear SVM, one weight vector per folder, over 5 features: how many times the words “free”, “meeting”, “unsubscribe” and “winner” appear, and the number of links. The email client encrypts the counts, so the service never reads the email, and the service never gives away the filter that spammers would love to reverse engineer.
This time, we pass our own callable instead of answer_argmax directly. It records every message the service sends to the user, which is exactly where a network call goes in production.
from lightphe import LightPHE # user builds the cryptosystem in the email client, and shares the public key user_cs = LightPHE(algorithm_name = "Damgard-Geisler-Kroigaard") user_cs.export_keys(target_file = "public.txt", public = True) # counts in the email: "free", "meeting", "unsubscribe", "winner" and links features = [2, 0, 1, 0, 3] encrypted_features = [user_cs.encrypt(x) for x in features] # service's one-vs-rest linear svm for primary, promotions and spam, quantized by 1000 service_cs = LightPHE(algorithm_name = "Damgard-Geisler-Kroigaard", key_file = "public.txt") classes = ["primary", "promotions", "spam"] float_weights = [ [-0.2, 0.9, -0.4, -0.8, -0.1], [0.5, -0.3, 1.1, 0.1, 0.3], [0.7, -0.5, 0.2, 1.4, 0.6], ] float_biases = [0.5, -0.6, -2.2] weights = [[round(w * 1000) for w in class_weights] for class_weights in float_weights] biases = [round(b * 1000) for b in float_biases] # every message to the user goes through this callable, e.g. a network call in production messages = [] def ask_user(message): messages.append(message["type"]) return user_cs.answer_argmax(message) encrypted_class = service_cs.evaluate_hyperplane( encrypted_features, weights, bit_length = 16, client = ask_user, biases = biases ) # user learns only the folder: promotions assert classes[user_cs.decrypt(encrypted_class)] == "promotions" # 3 round trips for each class after the first one assert len(messages) == 6
package main
import (
"fmt"
"log"
"math"
"math/big"
"github.com/serengil/lightphe-go/lightphe"
)
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
// user builds the cryptosystem in the email client, and shares the public key
userCs, err := lightphe.New(lightphe.DamgardGeislerKroigaard)
check(err)
check(userCs.ExportKeys("public.txt", true))
// counts in the email: "free", "meeting", "unsubscribe", "winner" and links
features := []int64{2, 0, 1, 0, 3}
var encryptedFeatures []*lightphe.Ciphertext
for _, x := range features {
c, err := userCs.EncryptInt(x)
check(err)
encryptedFeatures = append(encryptedFeatures, c)
}
// service's one-vs-rest linear svm for primary, promotions and spam, quantized by 1000
serviceCs, err := lightphe.New(lightphe.DamgardGeislerKroigaard, lightphe.WithKeyFile("public.txt"))
check(err)
classes := []string{"primary", "promotions", "spam"}
floatWeights := [][]float64{
{-0.2, 0.9, -0.4, -0.8, -0.1},
{0.5, -0.3, 1.1, 0.1, 0.3},
{0.7, -0.5, 0.2, 1.4, 0.6},
}
floatBiases := []float64{0.5, -0.6, -2.2}
weights := make([][]int64, len(floatWeights))
biases := make([]int64, len(floatBiases))
for i, classWeights := range floatWeights {
for _, w := range classWeights {
weights[i] = append(weights[i], int64(math.Round(w*1000)))
}
biases[i] = int64(math.Round(floatBiases[i] * 1000))
}
// every message to the user goes through this function, e.g. a network call in production
var messages []string
askUser := func(message lightphe.ArgmaxMessage) ([]*big.Int, error) {
messages = append(messages, message.Type)
return userCs.AnswerArgmax(message)
}
encryptedClass, err := serviceCs.EvaluateHyperplane(encryptedFeatures, weights, 16, askUser, biases)
check(err)
// user learns only the folder: promotions
class, err := userCs.Decrypt(encryptedClass)
check(err)
fmt.Println(classes[class.Int64()]) // promotions
// 3 round trips for each class after the first one
fmt.Println("round trips:", len(messages)) // round trips: 6
}
import io.github.serengil.lightphe.LightPHE;
import io.github.serengil.lightphe.model.Ciphertext;
import java.math.BigInteger;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
// user builds the cryptosystem in the email client, and shares the public key
LightPHE userCs = LightPHE.builder().algorithmName("Damgard-Geisler-Kroigaard").build();
userCs.exportKeys(Path.of("public.txt"), true);
// counts in the email: "free", "meeting", "unsubscribe", "winner" and links
List<Long> features = List.of(2L, 0L, 1L, 0L, 3L);
List<Ciphertext> encryptedFeatures = features.stream().map(userCs::encrypt).toList();
// service's one-vs-rest linear svm for primary, promotions and spam, quantized by 1000
LightPHE serviceCs = LightPHE.builder()
.algorithmName("Damgard-Geisler-Kroigaard")
.keyFile(Path.of("public.txt"))
.build();
List<String> classes = List.of("primary", "promotions", "spam");
double[][] floatWeights = {
{-0.2, 0.9, -0.4, -0.8, -0.1},
{0.5, -0.3, 1.1, 0.1, 0.3},
{0.7, -0.5, 0.2, 1.4, 0.6},
};
double[] floatBiases = {0.5, -0.6, -2.2};
List<List<Long>> weights = new ArrayList<>();
List<Long> biases = new ArrayList<>();
for (int i = 0; i < floatWeights.length; i++) {
List<Long> classWeights = new ArrayList<>();
for (double w : floatWeights[i]) {
classWeights.add(Math.round(w * 1000));
}
weights.add(classWeights);
biases.add(Math.round(floatBiases[i] * 1000));
}
// every message to the user goes through this function, e.g. a network call in production
List<String> messages = new ArrayList<>();
Ciphertext encryptedClass = serviceCs.evaluateHyperplane(
encryptedFeatures,
weights,
16,
message -> {
messages.add(message.type());
return userCs.answerArgmax(message);
},
biases);
// user learns only the folder: promotions
assert classes.get(userCs.decrypt(encryptedClass).intValue()).equals("promotions");
// 3 round trips for each class after the first one
assert messages.size() == 6;
import { LightPHE, type ArgmaxMessage } from 'lightphe-ts';
// user builds the cryptosystem in the email client, and shares the public key
const userCs = new LightPHE({ algorithmName: 'Damgard-Geisler-Kroigaard' });
userCs.exportKeys('public.txt', true);
// counts in the email: "free", "meeting", "unsubscribe", "winner" and links
const features = [2, 0, 1, 0, 3];
const encryptedFeatures = features.map((x) => userCs.encrypt(x));
// service's one-vs-rest linear svm for primary, promotions and spam, quantized by 1000
const serviceCs = new LightPHE({ algorithmName: 'Damgard-Geisler-Kroigaard', keyFile: 'public.txt' });
const classes = ['primary', 'promotions', 'spam'];
const floatWeights = [
[-0.2, 0.9, -0.4, -0.8, -0.1],
[0.5, -0.3, 1.1, 0.1, 0.3],
[0.7, -0.5, 0.2, 1.4, 0.6],
];
const floatBiases = [0.5, -0.6, -2.2];
const weights = floatWeights.map((classWeights) => classWeights.map((w) => Math.round(w * 1000)));
const biases = floatBiases.map((b) => Math.round(b * 1000));
// every message to the user goes through this function, e.g. a network call in production
const messages: string[] = [];
const askUser = (message: ArgmaxMessage) => {
messages.push(message.type);
return userCs.answerArgmax(message);
};
const encryptedClass = await serviceCs.evaluateHyperplane(encryptedFeatures, weights, 16, askUser, {
biases,
});
// user learns only the folder: promotions
console.assert(classes[Number(userCs.decrypt(encryptedClass))] === 'promotions');
// 3 round trips for each class after the first one
console.assert(messages.length === 6);
In plain floats, the scores of primary, promotions and spam are -0.6, 2.4 and 1.2, so the email goes to promotions. With 3 classes, the argmax took 6 round trips, 3 for each class after the first one. In TypeScript, evaluateHyperplane is asynchronous, so the callable can just as well return a promise from a fetch call. Java snippets use assert for brevity, so run them with -ea to enable assertions.
More Scenarios
- Credit scoring: A customer checks loan eligibility at several banks before applying anywhere. Each bank runs its own model on the encrypted financials, so the customer shops around without spreading income and debt details across the industry, and no bank exposes its credit policy.
- Medical risk prediction: A hospital’s logistic regression model predicts diabetes risk from glucose level, BMI, age and blood pressure. A patient, or a small clinic without its own model, gets the risk class without sending health records to the hospital, and the hospital keeps a model trained on thousands of patients to itself.
- Spam and phishing filtering: As in our second example, a mail provider classifies encrypted word counts and link statistics, so it never reads the email, and attackers cannot query the filter’s weights to craft messages that slip through.
- Fraud detection: A payment network scores a merchant’s encrypted transaction features with its linear fraud model. The merchant learns only whether the transaction is risky, without sharing its customers’ details, and fraudsters cannot study the model through the merchant.
- Insurance quotes: An insurer’s linear model assigns a risk tier from driving history and vehicle data. A comparison site gets the tier for its user without the insurer seeing the data before the user decides to buy.
Caveats
- Semi-honest parties: The protocol assumes both sides follow the steps and are only curious. A malicious server could, for example, evaluate a different model than it claims.
- Labels still leak a little: The customer learns the predicted class, and with many thousands of queries, an attacker can still approximate a decision boundary from labels alone. Rate-limit queries per customer, as you would for any prediction API.
- Quantization: Rounding weights can flip predictions for samples sitting right on the decision boundary. Compare quantized and float predictions on your validation set, and use a larger scale if needed, as long as scores fit into about 30 bits.
- Latency: Scores are cheap, but the argmax costs 3 round trips per extra class, and the client must stay online during the protocol. A binary classifier is fast; a 100-class model over a slow network is not.
Conclusion
Logistic regression and linear SVM look different on paper, but at prediction time both are an argmax over linear scores. That splits encrypted inference into two parts. Scores are a weighted sum, so any additively homomorphic cryptosystem with scalar multiplication computes them without the private key. The argmax is the hard part: decrypting the scores would leak the model, so the classes must be compared under encryption, with a few masked round trips to the customer. That second part needs a small plaintext space, a cheap zero test and secure comparison, which is why Damgard-Geisler-Kroigaard is the cryptosystem for encrypted hyperplane classification in LightPHE.
The result is a prediction where the customer learns only the class, the model owner learns nothing about the data, and nobody had to trust anybody—all in a few lines of Python, Go, Java or TypeScript.
Support this blog financially if you do like!
