Architecture

OpenMed separates into two planes. The learning plane produces models from data that never leaves its institution. The trust plane decides which models are admitted, records what happened, and makes that record checkable by someone who trusts none of the participants.

Two planes

PlanePackagesAnswers
Learning trustfed.data, trustfed.models, trustfed.federated, trustfed.aggregation, trustfed.attack, trustfed.benchmark How do sites train one model together when a minority of them may be malicious?
Trust trustfed.attestation, trustfed.ledger, trustfed.registry, trustfed.certification, trustfed.quality, trustfed.incentives Which models are admitted, on whose authority, and how does an outsider verify that later?

The planes meet at two points only: the attestation gate that admits an update into aggregation, and the model bundle that carries a trained model into the registry. Everything else is independent, which is why a real TEE backend or a different federated framework can be swapped in without touching the rest.

End-to-end flow

site-local data  ──▶  Client.train()
                          │  update + attestation quote
                          ▼
                   Attestor.check()   ──▶ rejected (unapproved code / replayed nonce*)
                          │ accepted
                          ▼
             aggregation (krum / median / trimmed mean / clipping)
                          │  new global model
                          ▼
                  ModelBundle  ──▶  ModelRegistry.publish()
                          │              │  event written to ledger
                          │              ▼
                          │        LineageGraph (parent ◀── derived)
                          ▼
                  QualityAnalyzer.analyze()  ──▶  QualityReport (JSON + Markdown)
                          │
                          ▼
       CertificationAuthority: k-of-n signatures across DISTINCT institutions
                          │  decision appended (tamper-evident)
                          ▼
                  certified base model  ──▶  CreditLedger (citable id, attribution)
                                                 │
                                                 ▼
                                          ReciprocityPolicy gates the next evaluation

* replayed-nonce rejection requires a NonceStore and require_nonce=True; both off by default

Federated co-training built

Server runs rounds: broadcast the global model, collect Updates, reject anything that fails attestation, aggregate the survivors with a Byzantine-robust rule, evaluate on held-out data, and record a RoundResult.

Aggregators in trustfed.aggregation, each with a documented tolerance bound:

RuleByzantine tolerance
fedavgNone — a single large update dominates. Included as the baseline that shows why the others are needed.
coordinate_medianFewer than half the clients
trimmed_meanUp to the trimmed fraction on each side
krum / multi_krumRequires n > 2f + 2
norm_clipped_mean, centered_clippingBounds each client's influence rather than excluding clients

Call tolerance_bound() or tolerance_table() to get the bound for a configuration rather than assuming it. Client selection is pluggable: RandomSelector, LossBasedSelector, and ReputationSelector, which consumes per-client history (ClientHistory) accumulated across rounds.

trustfed.attack exists so defenses are tested against something real: sign flipping, gaussian noise, scaling and model replacement, label flipping, feature corruption, and AdaptiveAdversary — an attacker that targets the specific aggregator in use. A defense that only survives the non-adaptive attacks has not been tested.

Attestation built

Before an update is aggregated, the client presents a Quote over its code measurement. AttestationPolicy holds the set of approved measurements; NonceStore enforces freshness so a captured quote cannot be replayed — when the attestor is built with one and the policy sets require_nonce=True. Both default to off, including in the shipped parkinson demo, and without them a captured quote replays indefinitely.

MockSoftwareAttestor is not a security boundary. The "measurement" is a hash of a declared identity string and the "quote" is an HMAC under a shared key standing in for a hardware root of trust. It is sufficient to exercise the protocol and to test accept/reject logic, and nothing more. Implement the Attestor interface against SGX/TDX or SEV-SNP quoting APIs for a real deployment — the rest of OpenMed is unchanged.

Tamper-evident ledger built

An append-only hash chain. Each Block carries its index, the previous block's hash, a canonical-JSON payload, the payload hash, and a signature. LedgerBackend.verify_chain() returns a structured ChainVerification that detects modification and reordering always. Truncation — the case naive implementations miss — is detected only against an anchor: FileLedger's .head.json sidecar, or an external Checkpoint passed as expected=. An unanchored verdict reports anchored=False and cannot rule out a missing tail; InMemoryLedger has no anchor.

  • InMemoryLedger for tests and short-lived runs; FileLedger (JSONL) for durable logs.
  • Signing is Ed25519Signer when cryptography is importable, falling back to HmacSigner. HAVE_CRYPTOGRAPHY tells you which you got. The HMAC fallback provides integrity under a shared secret — it is not a signature and does not give non-repudiation.
  • LedgerBackend is an ABC, so a permissioned-ledger backend can be added without changing callers.

The registry, certification authority, and credit ledger all write through this interface, which is what makes their histories jointly checkable.

Model registry built

A ModelBundle is the unit of exchange: a WeightsRef, a ModelCard (following the Mitchell et al. sections), a PipelineAttestation, an EvaluationReport, and a FineTuningManual. Bundle IDs are content-addressed, so the identifier commits to the contents.

Publishing a model derived from another auto-links it to its parent. The registry therefore accumulates a LineageGraph rather than a flat file list, and ModelRegistry.verify_lineage() walks the chain to a root and returns a LineageVerdict naming each LineageIssue it found — a missing parent, an unverifiable attestation, a broken hash. One call answers "is this model's whole ancestry attested?" (It is a method, ModelRegistry.verify_lineage(), not a free function.)

Multi-party certification built

A model becomes a certified base only when reviewers from more than one institution sign off. ThresholdPolicy enforces k-of-n over distinct institutions, with the rules that make it meaningful:

  • A site's signature does not count toward its own submission's quorum — ConflictOfInterestError.
  • Reviewer keys are institution-bound via ReviewerKeyring; an unknown key is rejected rather than ignored.
  • Every decision is appended to the ledger. A revocation is a new entry; the original stays visible.

A CertificationCase moves through an explicit state machine — submitted → under_review → blocked | certified → revoked, with remediated as the path back from blocked. Illegal transitions raise InvalidTransitionError rather than silently succeeding; transitions_table() prints the whole machine.

validate_manual() holds the fine-tuning manual to a fixed schema — intended use, data, preprocessing, hyperparameters, failure modes, clinical caveats — so what a hospital receives is a clinically actionable model rather than a checkpoint.

Automated quality analysis built

QualityAnalyzer runs independent Checks over a bundle and emits a QualityReport (JSON and Markdown). Shipped checks:

CheckCatches
ModelCardCompletenessCheckMissing required model-card sections
MetadataSchemaCheckStructurally invalid or malformed bundle metadata
MetricsPresentCheckAbsent, out-of-range, or implausible reported metrics
SubgroupGapCheckPerformance gaps across reported subgroups
MembershipInferenceCheckTraining-data leakage, via a global-threshold attack on confidence scores. Skips unless member/non-member scores are supplied; a pass means this attack failed, not that the model is private
ParameterNormCheckParameter-norm outliers relative to the family
LineageAttestationCheckMissing or unverifiable provenance

Summarisation goes through the LLMSummarizer protocol. TemplateSummarizer is the default: deterministic, no model required, so the gate runs anywhere. LocalLLMSummarizer is optional and uses a locally hosted endpoint, so no submission leaves the reviewing site. The report carries a DISCLAIMER stating that automated analysis supplements human review rather than replacing it.

Credit and reciprocity built

CreditLedger records CreditEvents on top of the ledger: accepted models, evaluations served, code contributions. It issues a CitableIdentifier and ReleaseAttribution for accepted work, and computes ContributorStanding — a StandingTier that maps to governance weight.

ReciprocityPolicy is the rule with teeth: to receive a multi-site evaluation, a site must have served as an evaluator. A request that does not qualify returns a ReciprocityDecision carrying a machine-readable reason, not a bare refusal — the requester is told exactly what would make it succeed.

Benchmark harness built

A Scenario declares sites, Byzantine count, attack, aggregator, rounds, and seed. Runner executes a grid() of them and produces a BenchmarkReport serialisable to JSON with a Markdown table. PROVENANCE records what produced a report, and compare_reports() diffs two runs.

The point is that a contributed defense is measured on the same scenarios as everything else, so results are comparable rather than self-reported. Runs are reproducible from the seed.

Extension points

These are the interfaces to implement if you want to extend OpenMed rather than fork it:

InterfaceImplement to add
AttestorA real TEE backend (SGX/TDX, SEV-SNP)
LedgerBackendA permissioned or distributed ledger
Signer / VerifierAn HSM or external key-management system
Aggregator callableA new robust aggregation rule (register it in AGGREGATORS)
SelectorA new client-selection strategy
CheckA new automated quality gate
LLMSummarizerA different local summarisation model
Attack callableA new attack for the benchmark harness

Adding an aggregator or an attack and opening a pull request against the benchmark harness is the shortest path from a research idea to something other people can reproduce. See community.