@atlasauth/pca-mpc
v0.4.3
Published
Multi-stakeholder policy VM composed under secure multi-party computation (semi-honest additive secret sharing over a prime field, malicious-with-abort SPDZ-style layer, post-quantum base-OT) with a mutual-TLS networked runner. Experimental reference prot
Maintainers
Readme
@atlasauth/pca-mpc
Experimental reference prototype: a multi-stakeholder policy VM composed under secure multi-party computation, for Proof-Carrying Authority (PCA). Several stakeholders (for example user, organization, regulator) each evaluate their own private PCA policy to a tiny decision vector { allow, t, rQuant }, then compose them under MPC so only the joint result is revealed: allow if every party allows, the maximum required proof threshold, and the maximum quantized risk. No party reveals its own policy or its own vector.
Install
npm i @atlasauth/pca-mpc @atlasauth/pcaUsage
import { generateKeyPair, encodeKey, mintGrant, DEFAULT_RISK_POLICY } from '@atlasauth/pca';
import { evaluateParty, composeSecure, composeSecureMalicious } from '@atlasauth/pca-mpc';
const principal = generateKeyPair();
const holder = generateKeyPair();
// Each stakeholder builds a PartyPolicy from its own grant + decision input.
function party(id: string, verb = '*') {
const { grant } = mintGrant({
principalSecret: principal.secretKey,
principalPublic: encodeKey(principal.publicKey),
holder: encodeKey(holder.publicKey),
goal: 'wire funds',
envelope: { predicates: [{ verb, resource: '*' }], caveats: [], agent_binding: {}, risk_policy: DEFAULT_RISK_POLICY },
});
return {
id,
decideInput: {
grant,
action: { action: { verb: 'wire', resource: 'acct:1', params: {} } },
risk: { semanticDistance: 0.1, reversibility: 1, blastRadius: 0.1, taint: 0, confidence: 1, age: 0 },
budget: { B: 1, tau: 0 },
now: Date.now(),
},
};
}
// Local step: each party reduces its private policy to a decision vector.
const vectors = [party('user'), party('org'), party('regulator', 'read')].map((p) => evaluateParty(p));
// Joint step: semi-honest MPC (additive sharing over a prime field + Beaver triples).
const result = composeSecure(vectors);
result.composed; // { allow: 0, t: 3, rQuant: 5 } (the regulator only permits 'read', so the joint answer is deny)
// Malicious-with-abort variant (SPDZ-style MACs): correct result, or a thrown abort.
const strict = composeSecureMalicious(vectors);API (in-process)
evaluateParty(policy, Q?)- reduce a party's private PCA decision input to aDecisionVector.composeSecure(vectors, config?)- semi-honest composition; returnscomposed, the public Beaveropenedmessages, and per-party views.composeSecureMalicious(vectors, config?)- dishonest-majority layer with information-theoretic MACs and a MAC check that aborts on deviation. The default offline phase is a TRUSTED DEALER (the result saysofflineMode: 'trusted-dealer'); every dealt triple is certified by the sacrifice check, so a dealer that lies aboutcaborts the run (triplesVerified: true). An optional no-dealer offline provider (makeNoDealerOffline, OT-based) replaces the dealer (offlineMode: 'no-dealer').composeClear(vectors)- the cleartext reference thatcomposeSecuremust equal.- Lower-level building blocks (field arithmetic, sharing, Beaver triples, oblivious transfer including a post-quantum KEM-based base OT) are exported too.
Networked runner (@atlasauth/pca-mpc/net)
The in-process API above simulates every party in one process. The net module runs each party as its OWN OS process that talks to the others only over sockets:
- Transport (
mesh.ts): TLS 1.3 with mutual authentication and pinned, per-run self-signed certificates (keys and certificates generated by theopensslCLI on the node that owns the key; only public certificates are exchanged). A peer is identified by its certificate fingerprint, never by a claimed id. Frames are length-prefixed, strictly parsed JSON with per-link sequence numbers, a session id and a round label. No cryptography is hand-rolled. - Fail closed, attributable aborts: a disconnect, timeout, malformed or oversize frame, duplicate or skipped sequence number, wrong round or session, or non-canonical field element aborts the run with a
NetAbortnaming the sender. The first failure poisons the mesh, wakes every pending receive, sends an abort frame to every other peer, and closes the sockets. Every receive has a timeout, so nothing hangs. Attribution comes only from a node's own first-hand evidence on its own link. An abort frame received from a peer is only that peer's claim: the resultingpeer-abortrecords the sending peer asblamed(or no one, for a second-hand relay), keeps the accusation separately asclaimedBlameafter validating it (an integer node id; kind and detail are checked and length-capped), and never treats it as attribution. When no culprit can be named,blamedis null andNetAbort.reasonsays why; a timeout records itsdeadlineMs; anequivocationfound by the signed gadget echo carries the two signed statements asevidence(R3-16). Per-link inboxes are capped in frames and bytes (a flooding peer is blamed withflood), reads are paused under backpressure, and the observation log is bounded. - Protocol (
protocol.ts): the malicious-with-abort SPDZ-style composition, distributed, with two offline modes that feed the same online phase.trusted-dealer(default, and labelled so in the results): one dealer process ships each party its own shares of two MAC keys, sacrifice-paired Beaver triples, authenticated input masks and pairwise tags.no-dealer(offline: 'no-dealer',offline-net.ts): no dealer process; the parties generate all of that themselves from oblivious transfer between every ordered pair (session-bound Ed25519 base OTs, KOS-checked IKNP extension, Gilboa multiplication). Online, the parties run the triple sacrifice (2 partners per triple),inputs(or the input-consistency opening),mul0..(all chains batched per round),out, a commit/open coin-flip for the MAC-check challenges, commit/open of each party's MAC residuals (one per key), and a transcript-digest echo. No output is decoded until the MAC-check and the echo pass. - Identifiable abort (
pa.ts): every share carries pairwise information-theoretic tags (BDOZ-styleT_ij = K_ij + Delta_j * x_i; dealt by the dealer, or generated from OT in the no-dealer mode). Each receiver verifies every opened share at once, so a party that lies about a share, sends a wrong tag or equivocates is NAMED by the honest verifiers; after a failed MAC-check a blame round names a party that forged a MAC share or a residual, or the dealer when every party verifies. - Base OT over the wire (
ot-net.ts): the WASM-backed Ed25519 base OT and the post-quantum endemic Module-LWE base OT, run between two hosts. - Runners:
node dist/net/party-main.js <config.json>(one node),launchLocal(n+1 local processes, optional TCP fault proxy), andscripts/pca-mpc-multihost.mjs(the same nodes on separate hosts over ssh; host entries are validated, commands are passed as argument vectors, and host keys are pinned withhostKeyentries, or explicitly trusted on first use with--insecure-accept-new-host-keys, which is open to a man-in-the-middle). The fault-injection hooks (behavior,frameFault) are rejected by the production entry point unless the config setsunsafeTestHooksand the environment setsPCA_MPC_UNSAFE_TEST_HOOKS=1; the multi-host runner exposes them only behind--unsafe-test-hooks.
import { launchLocal } from '@atlasauth/pca-mpc/net';
const { outputs } = await launchLocal({ n: 3, Q: 100, vectors, mainJs: require.resolve('@atlasauth/pca-mpc/dist/net/party-main.js') });What the tests assert (src/net/net.test.ts, 43 tests)
- Honest multi-process runs (n = 1..4, Q = 16 and 100) return exactly
composeClearandcomposeSecureMalicious; every party computes the same transcript digest. - A pinned-identity check rejects an unpinned client certificate and a server impersonator.
- A malicious sender (party 1) that duplicates, reorders, skips a sequence number, sends garbage / truncated / oversize / extra-field / wrong-round / wrong-session frames, drops a frame, or goes silent makes every honest party abort with no output; each party that sees the fault first-hand names party 1.
- A malicious node that sends a forged abort frame accusing an honest party is blamed itself by the nodes that receive it; the accused is never blamed on that basis, and the accusation is retained only as
claimedBlame. - A malicious party that lies about an opened value (in a multiplication, output or sacrifice round), sends a wrong pairwise tag, equivocates, forges its MAC share or opens a wrong MAC residual makes every honest party abort with no output and is NAMED by the honest verifiers - each of the three parties in turn is made the cheater for seven kinds of lie (
src/net/identify.test.ts, 27 tests). One that opens a coin seed or a MAC residual inconsistently with its commitment, sends a wrong-length array or a non-canonical field element, crashes mid-protocol, or vanishes is named too. - A trusted dealer that deals a wrong product
c = a*b + 1(also in only one sacrifice partner), or a MAC that does not match its value, makes every party abort and is named as the dealer. - In the dealer-free mode (
src/net/offline-net.test.ts, 9 tests, no dealer process exists): honest runs for n = 2, 3, 4 equalcomposeClear; a party that lies online is named; a wrong operand in a triple-product OT multiplication aborts at the sacrifice (detected, not attributed except for two parties; in process it is attributed, see below); an inconsistent OT-extension matrix is named by the KOS check; a bad base-OT proof is named; a crash mid-offline is named. - A TCP-level adversary (bit-flip, truncate, duplicate, reorder in either direction, black-hole drop, long delay) is rejected by TLS record authentication or the receive timeout; all parties abort with no output and blame falls on the endpoints of the faulty link. A delay below the timeout is tolerated and the result is still correct.
- The same abort behaviours reproduced during the real multi-host run are recorded below.
Local multi-process evidence (2026-10-09, current build)
docs/evidence/pca-mpc-nodealer-local-2026-10-09.json, produced by scripts/pca-mpc-local-evidence.mjs (provenance, machine, source commit and the exact command are inside; paths and addresses scrubbed). Every node is its own OS process over mutual TLS on loopback (single machine, so no WAN latency). Q = 100: the trusted-dealer run and the dealer-free runs for 2, 3 and 4 parties all equal composeClear with matching transcript digests; the file also records the dealer lying about c, each party lying online in both modes, a wrong triple-product operand and a bad OT-extension matrix.
Multi-host evidence (2026-10-08, earlier build)
This run predates the two-key MAC-check, the sacrifice-certified dealer and the pairwise tags (single MAC key, uncertified dealer triples); it has not been repeated on the current build. Run on 2026-10-08 over the public internet, four Azure Standard_D2s_v4 VMs (Ubuntu 24.04, Node 22) in eastus, spaincentral, japaneast and westus3, installing the packed @atlasauth/pca-mpc tarballs (with the -wasm and -lwe-ot-wasm packages) on each. Parties 0, 1, 2 ran in eastus, spaincentral, japaneast; the dealer in westus3. Each host generated its own key and certificate. Raw transcripts (per-round timings, bytes, digests, per-link byte counts; addresses scrubbed): docs/evidence/pca-mpc-multihost-2026-10-08.json. TCP-connect RTTs measured between the hosts: 50 ms (eastus-westus3), 79 ms (eastus-spaincentral), 109 ms (westus3-japaneast), 157 ms (eastus-japaneast), 226 ms (spaincentral-japaneast).
| Run | Result |
| --- | --- |
| 3 parties, Q = 100, canonical user/org/regulator vectors | all three parties output {allow:1, t:2, rQuant:55} (= composeClear); the 3 parties' transcript digests are identical; 1.2-1.4 s per party end to end, ~12.6 KB sent and received per peer link, ~37 KB of shares from the dealer |
| 3 parties, four more vector sets (a denying party, three random) | every party's output equals MP-SPDZ MASCOT's output for the same inputs (5/5 cases agree) |
| 4 parties across 4 regions | all output {allow:1, t:3, rQuant:77} |
| Party 1 lies about an opened value (open-lie) | all three parties abort with mac-check, no output, no culprit named (this build had one MAC key and no pairwise tags; the current build names the cheater) |
| Party 2 duplicates a frame | party 0 and 1 abort duplicate, each blaming 2 from its own link |
| Party 1 crashes mid-protocol | parties 0 and 2 abort disconnect blaming 1 |
| Party 0 opens a coin seed that does not match its commitment | parties 1 and 2 abort commitment blaming 0 |
| Party 2 goes silent | parties 0 and 1 abort timeout blaming 2 after the timeout |
| WASM Ed25519 base OT, 128 transfers, eastus (sender) to japaneast (receiver) | receiver's pads equal the sender's pads at the receiver's choices; 0.8 s |
| WASM post-quantum endemic Module-LWE base OT, 128 transfers, same hosts | pads match; 2.1 s |
The independent cross-check (scripts/pca-mpc-mpspdz-xcheck.py) runs the same arithmetic circuit (AND of allow bits, position-wise OR of thermometers) under MP-SPDZ v0.4.3 mascot-party.x and compares the decoded decision. It cross-checks the composition semantics and the arithmetic, not the networking or the MAC-check implementation.
What the network runs prove, and what they do not
They show that the protocol logic computes the right composed decision when its parties are separate processes, talking only through pinned mutual TLS (on loopback in the current build's evidence, over the public internet in the 2026-10-08 build's), that it agrees with an independent implementation (MP-SPDZ MASCOT) of the same arithmetic circuit (2026-10-08 build), and that the abort behaviours listed fail closed in practice rather than only in an in-process model.
They do NOT prove security. There is no formal proof for this implementation, and the cryptography is unaudited. See "Security parameters and what is closed" for the statistical figures and the residuals. The transcript-digest echo is a hash comparison, not Byzantine agreement. Timing is not constant-time end to end, no resistance to traffic analysis or denial of service is claimed, and the two base-OT demos reveal the receiver's choices at the end to let the sender check them (a test-only step). Evidence inputs are synthetic test vectors.
Security parameters and what is closed
Statistical soundness (derivation, caveats and the tiny-field experiment: docs/pca/security/pca-mpc-spdz-soundness.md):
| Check | Parameter | Failure probability |
| --- | --- | --- |
| SPDZ MAC-check | 2 independent global keys, independent challenge vectors, one batched check; the engine refuses a context below 100 bits | at most (2/p)^2, about 2^-120 |
| Beaver-triple sacrifice (dealt and dealer-free triples) | 2 independent partners per triple | at most p^-2, about 2^-122 over the network; about 2^-120 in-process (e = 0 is remapped to 1); per triple, before the union bound over triples |
| KOS OT-extension check | kappa = 128, statistical parameter s = SSEC = 128 extra check rows (was 64); weights are SHAKE256 over the whole u matrix and the channel binding | best known attack (Roy, CRYPTO 2022, ePrint 2022/192) about 2^-128; argued from the attack analysis, NOT proved (the KOS proof is open; SoftSpokenOT's own check is not implemented) |
| Selective failure of the OT sender (RT-04) | OT choice bits are privacy-amplified: 61 + 3s = 253 uniform bits per share (s = 64, enc.ts; s < 40 refused) | leakage about 2^-63 per attacked sharing (argued, docs/pca/security/pca-mpc-spdz-soundness.md section 7) |
| Pairwise tags (identification only) | one field element per tag | 1/p per verified tag; the MAC-check still aborts if a lie slips through |
Closed in the unreleased build: RT-04 - in the dealer-free offline phase an OT sender could make the run abort exactly when a bit of the honest receiver's share was 1, which leaked an honest input bit (the red-team PoC recovered it 11 of 11 times). Receivers now choose with privacy-amplified bits (MASCOT), src/selective-failure.test.ts re-runs the attack for 200 runs and requires chance-level recovery (with a positive control on the legacy encoding), and an in-process offline abort now names the cheating sender (OfflineCheatingAbort.blamed). With the later OT-extension rewrite the dealer-free offline phase is much faster than before the fix: per authenticated triple, 2 parties 12-13 ms, 3 parties 34 ms in process (bench/triples.ts; was 257-417 ms and 593-637 ms at the same s = 64 before the rewrite). See CHANGELOG.md.
Closed in the previous release (each with failing-first tests; docs/pca/security/audit-crypto-packages.md, section 2b items 36-41): base-OT Schnorr proofs are bound to session, party indices, round and both points; base-OT keys hash the whole transcript; the KOS weights come from SHAKE256 (and the old code did not bind the high bits of u); the MAC-check has about 2^-120 soundness; a dealer's wrong triple is detected and the dealer is labelled in the API and results; the dealer-free offline phase runs over the mutual-TLS network; online cheaters are named.
Closed in the unreleased build (round 3): R3-16 the joint gadget is agreed by a signed commit / reveal / echo (src/net/gadget.ts), so one corrupt party can no longer give honest parties different gadgets and make them blame each other; R3-17 the forensic disclosures are commit-first (forensic-c, then forensic-o) with a shared deadline, so a rushing OT sender cannot tune its disclosure to the honest ones; aborts that cannot name a culprit carry blamed = null and a reason.
Remaining (honest): a formal proof and an independent review do not exist; the commit-then-open is a SHA-256 commitment and the public coin a hash-based coin flip; in trusted-dealer mode the dealer sees every share and key; in the networked dealer-free mode a wrong operand in a triple product is now attributed by a post-abort audit of the discarded triple's OT transcript (a failing pair names its counterpart only to the honest endpoint, a third party gets blamed = null); the offline phase still costs more than the dealer mode (the Q = 100, 3-party figure of about 90 s against about 1 s predates the speed-up and was not re-measured); the selective-failure bound is 2^-63 per attacked sharing, statistical and argued, not 2^-120, and assumes the sender's deviations do not depend on the public gadget; accusations are evidence for the accuser, not proof for a third party; the 2026-10-08 two-region run was not repeated on the new build; constant-time arithmetic is not provided.
Status
This is a research prototype, not a production MPC system, and stays one. The in-process runners exist for logic and tests; the net module adds a real mutual-TLS transport and multi-process / multi-host execution, with the limits stated above. The semi-honest layer assumes parties follow the protocol and uses a trusted dealer for triples; the malicious layer detects deviation by aborting, certifies dealt triples with the sacrifice, names online cheaters, and can use an OT-based offline phase (in process, and now over the network) instead of a dealer. The cryptography has not been audited. The composed verdict informs a decision; it does not authorize anything on its own - the resource server's verifier decides.
License
MIT - see LICENSE
