Midas Labs · notes · 2026-11-07

What does a signature on an embedding actually prove?

It proves who shipped the vector. It cannot prove the vector encodes the text — and every check that tries, inside a cosine tolerance, hands the attacker a measured number of queries.

Source: arXiv:2611.06498v1 · code securagenai/signed-but-steerable (v1.0.2, Zenodo 23948839) · 7 min read

0.75 → 0.39. On a Raspberry Pi 5 running a local language model, a knowledge pack that passed every check the device could perform cut correct answers by half, because the vectors it shipped were chosen, not computed.

The check that let it through is not a broken version of verification. The tolerance inside it is the attack surface, and on the hardware people actually deploy it is 25.3° — five times the median retrieval margin of the corpus it is protecting.

What the paper actually measured

Offline RAG on a phone-class device has an awkward property: the device cannot afford to compute embeddings. An earlier measurement in this line of work put 950 passages at roughly 29 minutes on a Pi CPU. So the knowledge pack arrives pre-computed — text, vectors, and a signature over the vectors. The signature is real cryptography, and it guarantees exactly one thing: these bytes came from that publisher. Nothing in it says the bytes encode the passages they sit next to.

An attacker who controls the pack (a compromised publisher, a mirror, a dependency) therefore has freedom: keep the text, keep a valid signature, and choose vectors that steer retrieval. The paper measures that freedom on three surfaces at once — the tolerance check on the device, the audit of compressed vector stores, and end-to-end answer quality — across a Pi 5 (16 GB, Cortex-A76, Qwen2.5-3B-Instruct Q4_K_M on CPU, ONNX Runtime embeddings), an RTX 4090 desktop, and an M2 Air. Corpus: SQuAD 2.0 (950 passages, 300 questions, 4,690 answerable spans) plus four BEIR sets and 1,000 MS MARCO queries.

The law that turns "run the attack" into arithmetic

Let m(q) be the angular margin — the gap between the gold document and its nearest competitor. A check that accepts a vector when cosine similarity stays within τ is, in angle, accepting anything inside arccos τ. An optimal attacker rotates the nearest non-gold vector toward the query by exactly that angle, so rank 1 flips precisely when m(q) < arccos τ. The hijack rate follows:

HR(τ) = |{ q : m(q) < arccos τ }| / |Q|

On 300 questions the law matched a simulated attack with a maximum absolute difference of 0.0. That is the useful part: you do not need to run the attack to know your exposure. You need your query log, your honest vectors, and one sort.

0%25%50%75%100% 0°10°20°30°37° tolerance angle median margin 5.2° FP32 noise 0.04° Pi INT8 threshold 25.3° BGE-M3 passages MiniLM windows
Indicative shape from Table 2. The point is the vertical distance between the curves and the dashed median: the threshold an operator must pick to avoid false alarms sits far to the right of where honest margins actually live.

Evidence 1: retrieval is decided by small angles

Median margin is 5.2° for BGE-M3 passage embeddings and 7.4° for MiniLM windows. Five percent of SQuAD queries sit below 0.6° and 0.8° respectively, and across corpora the median lands between 2.1° and 7.4°, with ArguAna lowest — 5% of its queries are decided by 0.02°. Any tolerance expressed in degrees is therefore not a small allowance. It is a multiple of the quantity it is supposed to tolerate.

Evidence 2: real quantisation pushes the tolerance above the margin

INT8 against FP32 gives a median cosine of 0.956. Taking the 0.1st percentile as a threshold that will not kill honest data yields 0.904, which is 25.3°: only 2 of 1,745 honest windows fall below it, the lowest at 0.898. That is the number an engineer would actually ship, and at it the hijack law predicts 100% on BGE-M3 and 98.4% on MiniLM; a transfer attack that never sees the original text lands 81.2% and 44.0%.

End to end on the Pi, the payload's share of top-3 retrieval goes from 2% at τ = 0.999 to 94% at the deployable threshold. Correct answers fall from 0.74 to 0.39 and refusals rise from 0.13 to 0.50 — the model behaves as though the corpus changed its mind.

Cost is what makes this worse rather than academic. Verifying 8 pairs per second slows concurrent decoding by 22.9–31.4% and pushes p99 inter-token latency up 108–167%; a saturated re-index costs 64.5–73.9% (baseline 5.14 tokens/s). Because the check competes with the model for the same CPU, it runs periodically — and the period is the lifetime of a poisoned payload.

Evidence 3: auditing compressed stores mostly fails, and fails absurdly

Set the tolerance to the maximum honest reconstruction error, so nothing false-positives, and measure how much of the tampered space still passes: FP16 0–5.2%, SQ8 2.0–14.5%, SQ4 55–89%, PQ 99.7–100%, binary 100%. On a real FAISS FiQA index of 57,638 documents: SQ8 gives a 0.57° tolerance and 8.3% exposure, PQ 37.97° and 100%, binary 44.64° and 100%. Running the PQ attack for real, all 60 tampered vectors pass the audit and the attacker document ranks first 25% of the time (CI 16–37%).

Three things the abstract will not tell you

1. The scariest number in the paper is a corpus artefact. The 5.5% ideal-precision bound is mostly duplicates: 18 of ArguAna's 23 exposed queries have a margin of exactly 0 because the competing document is the gold text verbatim, and NFCorpus behaves the same. Remove the duplicates and the worst case drops to 1.2% (5 of 420). The real story is not "it leaks 5%"; it is that at a threshold you can deploy, it leaks everything.

2. The compression that destroys tolerance audits is what makes code-level verification portable. PQ and binary cells are coarse enough to absorb floating-point noise: a guard width of 3e-7 for MiniLM SQ8 and 3e-6 for BGE-M3 SQ8 admits every honest vector across three platforms and four backends, leaving a publisher choice space of 0% and ≤0.16% respectively, and zero with canonicalisation. The industry trend toward harder quantisation helps here instead of hurting.

3. "Recompute and discard" beats "recompute and compare", and it is cheaper. Re-embedding 1,745 windows on the Pi takes 72.9 s in FP32 and 41.6 s in INT8, after which the publisher's numbers are irrelevant. The price is that the device needs an embedder. Bit-exact equality looks cleaner and is the hardest of all: none of the four FP32/INT8 pipelines reproduced results exactly — PyTorch varied by ±1e-7 with batch order, ONNX Runtime differed 1.9e-7 between x86 and ARM, and dynamic INT8 moved up to 0.03 with the same batch. Determinism is a security property, not a performance detail.

TODAY signed vectors publisher computes tolerance check accept if cos ≥ τ attacker gets arccos τ 25.3° on Pi INT8 ALTERNATIVE recompute locally on-device embedder compare codes guard 3e-6, not degrees publisher choice → 0 discard their numbers
Both paths keep the signature. Only the lower one removes the attacker's degrees of freedom.

Who this lands on

If you ship an offline or on-device RAG pack, the signature is an audit trail, not a defence. Either compute vectors on the device and compare codes with a fixed guard width, or accept that the publisher chooses your retrieval and treat the pack as trusted input — which it is, whether or not the check exists. The measuring stick is cheap: take your own index and query log, sort the margins, and look at the angle below which 5% of queries are decided. That number is your tolerance budget, and it will not be measured in degrees for long.

Disclosure: the authors are affiliated with securagen.ai, which sells tooling in this area; the 0.904 threshold describes one INT8 configuration and should not be generalised to other quantisation schemes without re-running the measurement. The paper's 5.5% headline bound is dominated by duplicate documents; the duplicate-free worst case is 1.2%.