Releases: BAder82t/fairlearn-fhe
v0.2.3 — security + correctness fixes
Highlights
Bug-fix release rolling up the security + correctness remediation from a code/security re-review of v0.2.2.
Security
- Secret-key isolation —
make_evaluator_contextnow serialises the inner TenSEAL Context without the secret key and rebuilds the wrapper from explicit fields. The keyholder's context is never mutated and shares no mutable state with the evaluator copy. - Sub-128-bit CKKS refused —
build_contextvalidates(poly_modulus_degree, coeff_mod_bit_sizes)against the HE-standard 128-bit table. Sub-128-bit configurations require explicitinsecure_allow_low_security=Trueand emitInsecureCKKSParametersWarning. - Signature downgrade closed — Signed envelopes write the canonical
signature_b64field.validate_enveloperejects legacyvalue-only blocks (closes the relay-strip downgrade vector).verify_envelope_signaturestill readsvaluefor back-compat verification of v0.2.2 envelopes. min_security_bitsfloor —validate_envelopedefaults to 128. Pass0to opt out.- CLI
--require-signatureenforces verification — Single error per failure mode (no key, key read failure, missing signature block, bad signature). No more silent satisfaction when only a signature block is present. - ANSI / Unicode sanitisation —
_safe_str(used byinspect) strips full CSI/OSC sequences and Unicode bidi / zero-width characters.
Correctness
- Per-thread
op_session()— New context manager withthreading.localstack. True isolation under concurrent audits — the previousreset_op_counters+ snapshot pattern interleaved across threads. - Encrypted-mask MSE depth fix —
_per_group_mse_termsre-foldsmask · swinto a single ct×pt before the ct×ct against ŷ², landing at depth 2 (was 3). - MAE opt-in —
mean_absolute_error_group_max(..., approximate=True)(default) acknowledges thesqrt(MSE)approximation;approximate=FalseraisesNotImplementedError. _safe_divconsolidated — Single canonical implementation in_circuitsused by base, scoring, and regression metrics.EncryptedVector.encryptbounds-checks — Rejects vectors larger thanctx.n_slotsinstead of silently truncating.- Scoring closures —
_group_min/_group_max/_group_difference/_group_ratiocapturereductionas an explicit closure variable. Plaintext fallback when fairlearn lacks the helper. audit_metricrecordsn_groups=0when no sensitive features.- Misc — Fixed double-indented
_build_encryptedbody, liftedfunctoolsimport out of hot loop, vectorisedgroup_masksfor single-column sensitive features.
Tests
Test fixture upgraded to N=16384 with an 8-prime chain (depth 6, 128-bit security). Two new regression suites — tests/test_review_v0_2_3_fixes.py and tests/test_review_v0_2_4_fixes.py — totalling 44 tests. 385 tests passing.
Migration
- Envelopes signed by v0.2.2 still verify cryptographically via
verify_envelope_signature. To passvalidate_envelopethey must be re-signed with v0.2.3 (the canonicalsignature_b64field is now required). - Callers building contexts with custom
coeff_mod_bit_sizesmay need to pick parameters from the HE-standard 128-bit table or passinsecure_allow_low_security=True.
Full diff: v0.2.2...v0.2.3
v0.2.2 - 100% line coverage
Patch release. 100% line coverage. No API changes.
Test coverage
- 341 tests pass (was 232 in v0.2.1).
- Line coverage: 100%.
What's covered now
- Mode B (encrypted-mask) paths for every v0.2 metric port (per-rate
_difference/_ratiofamily, scoring disaggregations, regression disaggregations). - Every
validate_envelopenegative branch: tampered hash, bad schema version, allowed-metric mismatch, depth ceiling/non-integer, security-bits floor, age ceiling, op_counts non-mapping/non-int, unsigned signature block, RSA-instead-of-Ed25519 signature. - Every CLI subcommand:
verify(stdin, oversized, signed, RSA-key TypeError, unsigned-with-key, legacy-positional),inspect(legacymetricfield, missing key),schema,doctor(with both backends, with one missing). - Every
aggregate_difference/aggregate_ratiobranch: empty input,between_groups,to_overallwith explicit and defaultoverall, zero-ref, zero-value, unknown method. - All scoring-helper zero-division edges (f1, zero-one-loss).
- All plaintext fallback paths in
_per_rate_metricsand_fairness_metrics(when upstream Fairlearn lacks the helper). make_derived_metricreserved-arg, unknown-transform, sample-param, and group-min/group-max paths._regression_metricszero-weight-group branch._json_safeover numpy scalars + nested containers + repr fallback.- OpenFHE backend
sum_allwindow-clamp branch.
Pinned defensive branches
Three branches are marked # pragma: no cover with inline documentation:
CKKSContext.make_evaluator_context()OpenFHE branch — theKeyPairbinding is unpickleable; the branch is pinned in tests as an expectedTypeError. A future fix (wrapper class aroundKeyPair) flips the pin from passing to failing._pos_neg_countshasattrguard —positives/negativesare dataclass fields, so the guard only fires for manually-constructed malformed instances.build_contextfinalValueError— unreachable becauseget_backendvalidates the backend name first.
Backward compatibility
No breaking changes. pip install --upgrade fairlearn-fhe.
v0.2.1 - test coverage closeout (86% → 94%)
Patch release. No API changes; extends test coverage and pins behaviour known to be broken so future fixes show up as flipped tests.
Test coverage
- 232 tests pass (was 154 in v0.2.0).
- Line coverage 86% → 94%.
What's covered now
- Mode B (encrypted-mask) paths for the v0.2 metric ports (per-rate
_difference/_ratiofamily, scoring disaggregations, regression disaggregations). - Plaintext fallback when upstream Fairlearn lacks
equal_opportunity_difference/_ratioand the per-rate_difference/_ratiohelpers. - Context lifecycle:
set_default_context,reset_default_context,make_evaluator_context(TenSEAL). - CLI residuals: stdin verify, oversize envelope, missing public key, legacy
metricenvelope key. - Audit edge paths:
SmallGroupWarningemission,no_sensitive_featurestrust-model label,encrypted_sensitive_featurestrust-model label, unknown-metricKeyError. validate_envelopenegative branches: tampered hash, bad schema version, allowed-metric mismatch, depth ceiling, age ceiling, security-bits floor.EncryptedVectoredges:__neg__,__sub__(ct−pt and ct−ct),mul_scalar,first_slot,__radd__.- Multi-column sensitive features (covers
_to_dataframearr.ndim==2branch).
Known issue pinned
CKKSContext.make_evaluator_context() on the OpenFHE backend currently raises TypeError: cannot pickle 'openfhe.openfhe.KeyPair'. The TenSEAL backend works correctly. A test pins the OpenFHE behaviour so a future fix (wrapper around KeyPair) flips the test from passing to failing instead of going unnoticed.
Backward compatibility
No breaking changes. pip install --upgrade fairlearn-fhe.
v0.2.0 - extended metric coverage + CLI + JSON Schema + threat model
Feature release on top of 0.1.0.
Highlights
Metric coverage
The encrypted metric catalog grows from 12 to 35 functions, closing
most of the gap with upstream Fairlearn:
- Per-rate _difference / _ratio family (10 new) —
selection_rate_difference/_ratio,
true_positive_rate_difference/_ratio,
true_negative_rate_difference/_ratio,
false_positive_rate_difference/_ratio,
false_negative_rate_difference/_ratio. - Scoring disaggregations (9 new) —
accuracy_score_difference,
accuracy_score_group_min,balanced_accuracy_score_group_min,
precision_score_group_min,recall_score_group_min,
f1_score_group_min,zero_one_loss_difference,
zero_one_loss_group_max,zero_one_loss_ratio. - Regression disaggregations (3 new) —
mean_squared_error_group_max,mean_absolute_error_group_max,
r2_score_group_min. MSE is exact (one ct×ct multiply forŷ²);
MAE is approximated assqrt(MSE)(exact for constant residuals,
upper bound otherwise — documented). selection_rate(pos_label=0)now works under encryption (computed
as1 - selection_rate(pos_label=1)). Otherpos_labelvalues still
raiseNotImplementedErrorwith a clear message.
Backend / context
- OpenFHE
noise_floodingis now wired throughbuild_context.
Passnoise_flooding="openfhe-NOISE_FLOODING_DECRYPT"(or
"noise-flooding",True) to enable OpenFHE'sEXEC_NOISE_FLOODING
execution mode where the linkedopenfhe-pythonbuild supports it.
Underscore / hyphen / case variants are normalised. TenSEAL ignores
the flag (no NOISE_FLOODING_DECRYPT mode in SEAL).
CLI
- New
fairlearn-fheentry point with subcommands:verify— validate an envelope (the original behaviour);inspect— pretty-print or JSON-dump an envelope summary
(--json);schema— emit the envelope JSON Schema (--prettyto indent);doctor— show backend availability.
- The legacy
fairlearn-fhe-verifyentry point is preserved and
maps directly tofairlearn-fhe verify. Existing CI scripts keep
working.
Envelope schema
ENVELOPE_JSON_SCHEMA(draft 2020-12) and
envelope_json_schema()factory are now exported at the package
level. Validators can check envelopes against a formal schema rather
than the bare version-tag string.
Documentation
- New Threat model
page formalising Mode A vs Mode B trust boundaries, what the auditor
learns vs does not learn, recommended deployments, and CKKS-specific
caveats. Linked frommkdocs.ymlnav.
Backward compatibility
No breaking changes vs 0.1.0. Every public import from 0.1.0 continues
to work; the new helpers are purely additive.
Quality
- 154 tests pass, ruff clean.
- Coverage 86%. Plaintext-fallthrough and Mode A paths of the new
metrics are covered; encrypted-mask Mode B paths for the new
regression / scoring helpers are wired but pinned to follow-up
fixtures.
Install
pip install --upgrade fairlearn-fhe- uses: BAder82t/fairlearn-fhe@v0.2.0Full changelog: see docs/changelog.md.
v0.1.0 - first release
First release of `fairlearn-fhe` — drop-in encrypted Fairlearn metrics
over CKKS via TenSEAL (default) or OpenFHE (opt-in).
Highlights
- 12 canonical Fairlearn metrics ported to CKKS:
`selection_rate`, `true_positive_rate`, `true_negative_rate`,
`false_positive_rate`, `false_negative_rate`, `mean_prediction`,
`demographic_parity_difference`, `demographic_parity_ratio`,
`equalized_odds_difference`, `equalized_odds_ratio`,
`equal_opportunity_difference`, `equal_opportunity_ratio`. - `MetricFrame`, `EncryptedMetricFrame`, and
`make_derived_metric` mirroring the Fairlearn API surface. - TenSEAL and OpenFHE backends behind a single `build_context`
dispatch. - Two trust models: encrypted `y_pred` with plaintext sensitive
features (Mode A, depth 1), or fully-encrypted `y_pred` and
sensitive features (Mode B, depth 2). - `MetricEnvelope` captures parameter-set hash, observed depth,
and op counts. `validate_envelope()` provides dependency-light
audit-envelope verification. - `fairlearn-fhe-verify` CLI for regulator-side envelope checks
without importing an FHE backend. - Optional Ed25519 envelope signing (`pip install
'fairlearn-fhe[signing]'`) plus signature verification. - Replay metadata records metric kwargs, trust model, and input
hashes so audits are reproducible. - `audit_metric()` one-call wrapper produces an audit envelope.
Numerical fidelity
Default settings yield `< 1e-4` absolute error vs the plaintext
Fairlearn baseline. The included benchmark
(`benchmarks/bench_metrics.py`, n=1024, 3 sensitive groups, depth-6
circuit) gives:
| backend | dp_diff | dp abs err | eo_diff | eo abs err |
|---|---|---|---|---|
| tenseal | 284 ms | 1e-7 | 562 ms | 2e-7 |
| openfhe | 505 ms | 2e-10 | 1015 ms | 4e-11 |
OpenFHE wins on numeric error; TenSEAL wins on speed and ships via
`pip` on every supported platform.
Install
```bash
pip install fairlearn-fhe # tenseal backend (default)
pip install 'fairlearn-fhe[openfhe]' # add openfhe backend (requires C++ build)
pip install 'fairlearn-fhe[signing]' # add Ed25519 envelope signing helpers
```
Quality
- 102 tests, 87% line coverage, ruff clean.
- Tests cover edge cases, envelope validation, backend dispatch,
encrypted arithmetic, CLI verification, and optional signature
verification.
Full changelog: see docs/changelog.md.