Dictionary of Applied Machine Learning · trusted execution environment (TEE)
Numerical companion to the entry trusted execution environment (TEE): it recomputes what the entry states and prints one line per check
Models the entry's Raspberry Pi in a clinic: a secure world that holds the model parameters of a learned hypothesis and the recorded heartbeats, a normal world that sees only the predictions, remote attestation by a signed digest of the code, and the two things a TEE does not protect by itself -- what the predictions reveal, and the side channel of a power trace. Self-contained (numpy/matplotlib and the standard library only), fixed seed.
Run it without installing anything:uv run https://dictionaryofml.org/terms/tee.py
uv downloads this script and the pinned NumPy and Matplotlib it needs, then runs it; the script fetches any input file it uses. To keep the output files, download tee.py into a folder and run uv run tee.py there. With NumPy and Matplotlib already installed, python3 tee.py, from any directory — it writes its output files into the current directory. Fixed seeds, so the printed numbers reproduce exactly. Download tee.py · Notebook · Open in Colab
One cell per block of the script: the code, and what that code printed when it last ran here
"""
tee.py — numerical companion to the glossary entry 'trusted execution
environment (TEE)'.
Purpose
-------
Models the entry's Raspberry Pi in a clinic: a secure world that holds
the model parameters of a learned hypothesis and the recorded heartbeats,
a normal world that sees only the predictions, remote attestation by a
signed digest of the code, and the two things a TEE does not protect by
itself -- what the predictions reveal, and the side channel of a power
trace. Self-contained (numpy/matplotlib and the standard library only),
fixed seed.
Setup
-----
Heartbeats with eight features (interval to the previous beat, width
of the QRS complex, height of the R peak, height of the T wave, and four
further measurements that carry no information about the label) and the
label arrhythmia / normal, generated by a fixed rule with noise. The
learned hypothesis assigns a score w^T x to a heartbeat; its model
parameters were obtained by training on 20 recorded heartbeats, and the
prediction is the sign of the score.
Blocks
------
[B-isolation] The secure world answers prediction requests; the normal
world's interface hands back the prediction and nothing
else, so the model parameters and the heartbeats are
not among the values it can obtain.
[B-attestation] The digest of the prediction code, signed with the
vendor key, verifies on the client; an altered code that
flags fewer arrhythmias changes the digest and fails
attestation.
[B-reveal] What the prediction reveals is not protected: on the 20
recorded heartbeats every score has the sign of the label
and a large size, while on new heartbeats the scores are
smaller and sometimes wrong, so a membership inference
attack that only observes predictions and knows the label
tells recorded from new heartbeats well above chance.
[B-sidechannel] A power trace modeled as the number of one-bits of each
operand in the multiplications of w^T x leaks the model
parameters: the traces of different heartbeats differ with
the feature values, and from them the sign and size of
each weight is recovered; constant-time code processes
every operand the same way and gives a flat trace.
Outputs
-------
tee_heartbeats.csv : the features, label and prediction of the heartbeats
the normal world can see (predictions only)
tee_scores.csv : signed scores y w^T x of recorded and of new heartbeats
tee_trace.csv : the power trace of the data-dependent and of the
constant-time code for one heartbeat
tee.png : matplotlib preview (checking only)
"""
import hashlib
import hmac
import inspect
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pathlib import Path
OUT_DIR = Path(__file__).parent
report = []
def check(name, ok):
report.append((name, bool(ok)))
print(f" [{'ok' if ok else 'FAIL'}] {name}")
rng = np.random.default_rng(7)
N_REC, N_NEW = 20, 200
FEATURES = ["beat interval (s)", "QRS width (ms)", "R height (mV)", "T height (mV)",
"P height (mV)", "ST level (mV)", "RR variability (s)", "noise level (mV)"]
def heartbeats(n):
"""Features and labels (1 = arrhythmia, -1 = normal) from a fixed model."""
y = np.where(rng.random(n) < 0.3, 1, -1)
x = np.c_[0.85 + 0.12 * rng.standard_normal(n) + 0.25 * (y == 1) * rng.standard_normal(n),
95 + 12 * rng.standard_normal(n) + 30 * (y == 1),
1.1 + 0.2 * rng.standard_normal(n) - 0.3 * (y == 1),
0.3 + 0.1 * rng.standard_normal(n) + 0.15 * (y == 1),
0.15 + 0.05 * rng.standard_normal(n), 0.05 * rng.standard_normal(n),
0.04 + 0.02 * rng.standard_normal(n), 0.02 + 0.01 * rng.standard_normal(n)]
return x, y
x_rec, y_rec = heartbeats(N_REC)
x_new, y_new = heartbeats(N_NEW)
mean, sd = x_rec.mean(0), x_rec.std(0)
def standardize(x):
return (x - mean) / sd
# the learned hypothesis: a score w^T x with model parameters trained on the recorded heartbeats
def train(x, y, steps=20000, lrate=0.5):
w = np.zeros(x.shape[1] + 1); xb = np.c_[standardize(x), np.ones(len(x))]
for _ in range(steps):
p = 1 / (1 + np.exp(-y * (xb @ w)))
w += lrate * (xb.T @ ((1 - p) * y)) / len(y)
return w
w_hat = train(x_rec, y_rec)
The secure world answers prediction requests; the normal world's interface hands back the prediction and nothing else, so the model parameters and the heartbeats are not among the values it can obtain.
class SecureWorld:
"""Holds the model parameters and the heartbeats; exposes predictions only."""
def __init__(self, w, code):
self.__w = w.copy(); self.__beats = []; self.__code = code
def predict(self, x):
self.__beats.append(x)
return self.__code(self.__w, standardize(np.atleast_2d(x))[0])
def digest(self):
return hashlib.sha256(inspect.getsource(self.__code).encode()).hexdigest()
def predict_code(w, xs):
return 1 if float(w[:-1] @ xs + w[-1]) > 0 else -1
class NormalWorld:
"""What the display software can do: ask for predictions."""
def __init__(self, secure):
self._secure = secure
def flag(self, x):
return self._secure.predict(x)
secure = SecureWorld(w_hat, predict_code)
normal = NormalWorld(secure)
preds = np.array([normal.flag(x) for x in x_new])
visible = [a for a in dir(normal) if not a.startswith("_")]
check(f"[B-isolation] the normal world's interface offers {visible} and hands back one prediction "
f"per heartbeat; the model parameters and the recorded heartbeats are not among its values",
visible == ["flag"] and set(preds) <= {-1, 1})
acc = float(np.mean(preds == y_new))
check(f"[B-isolation] the predictions it receives flag arrhythmias with accuracy {acc:.2f} "
"on 200 new heartbeats", acc > 0.8)
with open(OUT_DIR / "tee_heartbeats.csv", "w") as fh:
fh.write("interval,qrs_width,r_height,t_height,p_height,st_level,rr_variability,noise,label,prediction\n")
for xi, yi, pi in zip(x_new, y_new, preds):
fh.write(",".join(f"{v:.3f}" for v in xi) + f",{yi},{pi}\n")
[ok] [B-isolation] the normal world's interface offers ['flag'] and hands back one prediction per heartbeat; the model parameters and the recorded heartbeats are not among its values [ok] [B-isolation] the predictions it receives flag arrhythmias with accuracy 0.81 on 200 new heartbeats
The digest of the prediction code, signed with the vendor key, verifies on the client; an altered code that flags fewer arrhythmias changes the digest and fails attestation.
VENDOR_KEY = b"vendor-signing-key" # known to the chip vendor and the client
def sign(digest):
return hmac.new(VENDOR_KEY, digest.encode(), hashlib.sha256).hexdigest()
def verify(digest, signature, expected_digest):
return hmac.compare_digest(signature, sign(digest)) and digest == expected_digest
expected = hashlib.sha256(inspect.getsource(predict_code).encode()).hexdigest()
d_ok = secure.digest()
check(f"[B-attestation] the client verifies the signed digest {d_ok[:12]}... of the prediction "
"code before sending heartbeats", verify(d_ok, sign(d_ok), expected))
def tampered_code(w, xs):
return 1 if float(w[:-1] @ xs + w[-1]) > 2.0 else -1 # flags fewer arrhythmias
tampered = SecureWorld(w_hat, tampered_code)
flags_ok = sum(normal.flag(x) == 1 for x in x_new)
flags_t = sum(NormalWorld(tampered).flag(x) == 1 for x in x_new)
d_t = tampered.digest()
check(f"[B-attestation] altered code that flags {flags_t} instead of {flags_ok} heartbeats has the "
f"digest {d_t[:12]}... and fails attestation", flags_t < flags_ok and not verify(d_t, sign(d_t), expected))
[ok] [B-attestation] the client verifies the signed digest c82a627b84ef... of the prediction code before sending heartbeats [ok] [B-attestation] altered code that flags 39 instead of 47 heartbeats has the digest f41f5706690b... and fails attestation
What the prediction reveals is not protected: on the 20 recorded heartbeats every score has the sign of the label and a large size, while on new heartbeats the scores are smaller and sometimes wrong, so a membership inference attack that only observes predictions and knows the label tells recorded from new heartbeats well above chance.
# the signed score y * score: positive and large when the prediction is right with
# a large score. An attacker who holds a heartbeat with its label and sees
# the score guesses "recorded" when the signed score exceeds a threshold.
signed = lambda x, y: y * (np.c_[standardize(x), np.ones(len(x))] @ w_hat)
m_rec, m_new = signed(x_rec, y_rec), signed(x_new, y_new)
thresholds = np.unique(np.r_[m_rec, m_new])
balanced = [0.5 * (np.mean(m_rec > t) + np.mean(m_new <= t)) for t in thresholds]
mia = float(max(balanced)); t_best = float(thresholds[int(np.argmax(balanced))])
check(f"[B-reveal] every recorded heartbeat has a positive signed score (smallest {m_rec.min():.1f}), while "
f"{int((m_new <= 0).sum())} of the {N_NEW} new ones have a negative signed score and the rest a typical signed score "
f"of {np.median(m_new):.1f} against {np.median(m_rec):.1f} on recorded ones; guessing 'recorded' above the "
f"signed score {t_best:.1f} is right {mia:.2f} of the time on a balanced mix, above chance 0.5",
m_rec.min() > 0 and mia > 0.65)
with open(OUT_DIR / "tee_scores.csv", "w") as fh:
fh.write("recorded,signed_score\n")
for v in m_rec: fh.write(f"1,{v:.4f}\n")
for v in m_new: fh.write(f"0,{v:.4f}\n")
[ok] [B-reveal] every recorded heartbeat has a positive signed score (smallest 5.5), while 38 of the 200 new ones have a negative signed score and the rest a typical signed score of 11.6 against 11.7 on recorded ones; guessing 'recorded' above the signed score 5.0 is right 0.66 of the time on a balanced mix, above chance 0.5
A power trace modeled as the number of one-bits of each operand in the multiplications of w^T x leaks the model parameters: the traces of different heartbeats differ with the feature values, and from them the sign and size of each weight is recovered; constant-time code processes every operand the same way and gives a flat trace.
def bits(v):
"""Number of one-bits of a value in 8-bit fixed point: the power the multiplier draws."""
return bin(int(round(abs(v) * 16)) & 0xFF).count("1")
def trace_data_dependent(w, xs):
"""One multiplication per feature; the operand bits leave a trace."""
return [bits(wk) + bits(xk) for wk, xk in zip(w[:-1], xs)]
def trace_constant_time(w, xs):
"""Every operand masked to a full word before use: the trace is flat."""
return [8 + 8 for _ in zip(w[:-1], xs)]
xs0 = standardize(x_new[:1])[0]
t_dep, t_ct = trace_data_dependent(w_hat, xs0), trace_constant_time(w_hat, xs0)
traces = np.array([trace_data_dependent(w_hat, standardize(x_new[i:i + 1])[0]) for i in range(50)])
bits_x = np.array([[bits(v) for v in row] for row in standardize(x_new[:50])])
recovered = traces - bits_x # what remains is the weights' contribution
check(f"[B-sidechannel] the traces of 50 heartbeats differ with the data (spread "
f"{list(traces.max(0) - traces.min(0))} per multiplication), and subtracting the operand bits of each "
f"heartbeat leaves the bits of the weights, {list(recovered[0])}, the same for every heartbeat",
bool(np.all(recovered == [bits(v) for v in w_hat[:-1]])))
check(f"[B-sidechannel] constant-time code gives the trace {t_ct} for every heartbeat",
len(set(t_ct)) == 1 and trace_constant_time(w_hat, standardize(x_new[1:2])[0]) == t_ct)
with open(OUT_DIR / "tee_trace.csv", "w") as fh:
fh.write("multiplication,data_dependent,constant_time\n")
for k, (a, b) in enumerate(zip(t_dep, t_ct), 1):
fh.write(f"{k},{a},{b}\n")
# -------------------------------------------------------------- preview
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12.5, 3.7))
for lab, mk, name in ((-1, "o", "normal"), (1, "^", "arrhythmia")):
m = y_new == lab
ax1.scatter(x_new[m, 0], x_new[m, 1], marker=mk, s=18, facecolors="none" if lab == 1 else "k",
edgecolors="k", label=f"{name}, {np.mean(preds[m] == lab):.2f} flagged right")
ax1.set_xlabel(FEATURES[0]); ax1.set_ylabel(FEATURES[1])
ax1.set_title("what the normal world sees: predictions"); ax1.legend(frameon=False, fontsize=7)
ax2.hist(m_rec, bins=20, histtype="step", color="k", label="recorded heartbeats")
ax2.hist(m_new, bins=20, histtype="step", color="k", ls="--", label="new heartbeats")
ax2.set_xlabel("signed score $y\\,\\mathbf{w}^\\top \\mathbf{x}$"); ax2.set_ylabel("number of heartbeats")
ax2.set_title("what the predictions reveal"); ax2.legend(frameon=False, fontsize=8)
k = np.arange(1, 9)
ax3.plot(k, t_dep, "k-o", ms=5, label="data-dependent code")
ax3.plot(k, t_ct, "k--s", ms=5, mfc="none", label="constant-time code")
ax3.set_xticks(k); ax3.set_xlabel("multiplication of $\\mathbf{w}^\\top \\mathbf{x}$"); ax3.set_ylabel("operand one-bits (power)")
ax3.set_title("the power trace side channel"); ax3.legend(frameon=False, fontsize=8)
fig.tight_layout()
fig.savefig(OUT_DIR / "tee.png", dpi=110)
n_ok = sum(ok for _, ok in report)
print(f"\n{n_ok}/{len(report)} checks pass")
print(f"wrote {OUT_DIR / 'tee_heartbeats.csv'}, {OUT_DIR / 'tee_scores.csv'}, {OUT_DIR / 'tee_trace.csv'}, {OUT_DIR / 'tee.png'}")
if n_ok != len(report):
raise SystemExit(1)
[ok] [B-sidechannel] the traces of 50 heartbeats differ with the data (spread [np.int64(5), np.int64(5), np.int64(4), np.int64(4), np.int64(4), np.int64(5), np.int64(4), np.int64(4)] per multiplication), and subtracting the operand bits of each heartbeat leaves the bits of the weights, [np.int64(5), np.int64(5), np.int64(5), np.int64(3), np.int64(4), np.int64(3), np.int64(5), np.int64(2)], the same for every heartbeat [ok] [B-sidechannel] constant-time code gives the trace [16, 16, 16, 16, 16, 16, 16, 16] for every heartbeat 7/7 checks pass wrote /Users/junga1/dictionaryappliedml/pythondemos/tee_heartbeats.csv, /Users/junga1/dictionaryappliedml/pythondemos/tee_scores.csv, /Users/junga1/dictionaryappliedml/pythondemos/tee_trace.csv, /Users/junga1/dictionaryappliedml/pythondemos/tee.png

B-sidechannel writes when the script runs