@atlasauth/pca-mpc 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +81 -3
  2. package/dist/beaver.js +4 -0
  3. package/dist/beaver.js.map +1 -1
  4. package/dist/compose.d.ts +9 -2
  5. package/dist/compose.js +24 -1
  6. package/dist/compose.js.map +1 -1
  7. package/dist/ec.d.ts +6 -3
  8. package/dist/ec.js +27 -9
  9. package/dist/ec.js.map +1 -1
  10. package/dist/enc.d.ts +46 -0
  11. package/dist/enc.js +88 -0
  12. package/dist/enc.js.map +1 -0
  13. package/dist/endemic-ot.js +24 -1
  14. package/dist/endemic-ot.js.map +1 -1
  15. package/dist/hybrid-ot.js +3 -0
  16. package/dist/hybrid-ot.js.map +1 -1
  17. package/dist/kem-ot.js +2 -0
  18. package/dist/kem-ot.js.map +1 -1
  19. package/dist/mascot.d.ts +81 -30
  20. package/dist/mascot.js +232 -113
  21. package/dist/mascot.js.map +1 -1
  22. package/dist/net/certs.d.ts +14 -0
  23. package/dist/net/certs.js +30 -0
  24. package/dist/net/certs.js.map +1 -0
  25. package/dist/net/faults.d.ts +49 -0
  26. package/dist/net/faults.js +222 -0
  27. package/dist/net/faults.js.map +1 -0
  28. package/dist/net/forensics.d.ts +81 -0
  29. package/dist/net/forensics.js +181 -0
  30. package/dist/net/forensics.js.map +1 -0
  31. package/dist/net/gadget.d.ts +44 -0
  32. package/dist/net/gadget.js +102 -0
  33. package/dist/net/gadget.js.map +1 -0
  34. package/dist/net/index.d.ts +11 -0
  35. package/dist/net/index.js +28 -0
  36. package/dist/net/index.js.map +1 -0
  37. package/dist/net/launch-local.d.ts +53 -0
  38. package/dist/net/launch-local.js +190 -0
  39. package/dist/net/launch-local.js.map +1 -0
  40. package/dist/net/launch-support.d.ts +6 -0
  41. package/dist/net/launch-support.js +65 -0
  42. package/dist/net/launch-support.js.map +1 -0
  43. package/dist/net/mesh.d.ts +135 -0
  44. package/dist/net/mesh.js +491 -0
  45. package/dist/net/mesh.js.map +1 -0
  46. package/dist/net/offline-net.d.ts +58 -0
  47. package/dist/net/offline-net.js +485 -0
  48. package/dist/net/offline-net.js.map +1 -0
  49. package/dist/net/ot-net.d.ts +42 -0
  50. package/dist/net/ot-net.js +219 -0
  51. package/dist/net/ot-net.js.map +1 -0
  52. package/dist/net/pa.d.ts +65 -0
  53. package/dist/net/pa.js +118 -0
  54. package/dist/net/pa.js.map +1 -0
  55. package/dist/net/party-main.d.ts +48 -0
  56. package/dist/net/party-main.js +146 -0
  57. package/dist/net/party-main.js.map +1 -0
  58. package/dist/net/prep.d.ts +40 -0
  59. package/dist/net/prep.js +12 -0
  60. package/dist/net/prep.js.map +1 -0
  61. package/dist/net/protocol.d.ts +126 -0
  62. package/dist/net/protocol.js +655 -0
  63. package/dist/net/protocol.js.map +1 -0
  64. package/dist/net/wire.d.ts +89 -0
  65. package/dist/net/wire.js +242 -0
  66. package/dist/net/wire.js.map +1 -0
  67. package/dist/ot.d.ts +176 -17
  68. package/dist/ot.js +479 -252
  69. package/dist/ot.js.map +1 -1
  70. package/dist/otx.d.ts +76 -0
  71. package/dist/otx.js +238 -0
  72. package/dist/otx.js.map +1 -0
  73. package/dist/runner.d.ts +1 -1
  74. package/dist/runner.js +6 -1
  75. package/dist/runner.js.map +1 -1
  76. package/dist/sharing.d.ts +2 -0
  77. package/dist/sharing.js +7 -4
  78. package/dist/sharing.js.map +1 -1
  79. package/dist/spdz-runner.d.ts +20 -3
  80. package/dist/spdz-runner.js +21 -3
  81. package/dist/spdz-runner.js.map +1 -1
  82. package/dist/spdz.d.ts +96 -22
  83. package/dist/spdz.js +196 -47
  84. package/dist/spdz.js.map +1 -1
  85. package/package.json +15 -6
package/dist/ot.js CHANGED
@@ -48,7 +48,14 @@
48
48
  * random weights `χ_j ∈ GF(2^κ)` (one per row, bound to `u` by a Fiat–Shamir hash here), the
49
49
  * receiver reveals `x = Σ_j b_j·χ_j` and `t = Σ_j χ_j·t_j`, and the sender checks
50
50
  * `Σ_j χ_j·q_j == t ⊕ x·s` in GF(2^κ). Honestly this always holds (`q_j = t_j ⊕ b_j·s`); a receiver
51
- * who deviated in `u` satisfies it with probability ≤ `2^{-κ} = 2^{-128}`, so a deviation ABORTS.
51
+ * who deviated in `u` is caught, so a deviation ABORTS. The honest statement of the bound is the KOS
52
+ * STATISTICAL parameter `s = SSEC = 128`, NOT `2^{-κ}`: a receiver that deviates in a few columns passes by
53
+ * guessing the matching bits of the selector `s`, and the 2015 proof (ePrint 2015/546) was additionally shown
54
+ * to rest on an incorrect lemma (Roy, CRYPTO 2022). Roy's attacks give the receiver success
55
+ * about `2^{-s}` with `s` extra rows, and the KOS proof is still open (Roy leaves a sound proof to future work), so
56
+ * this module sets `s = SSEC = 128` extra rows (was 64): the best KNOWN attack then succeeds with probability about
57
+ * `2^{-128}`. That is an argument from the attack analysis, NOT a proof; we did not implement SoftSpokenOT's own
58
+ * (different, small-field VOLE) consistency check, whose parameters we could not verify from the paper.
52
59
  * The check is run over `m + κ + SSEC` rows — the extra `κ + SSEC` random "sacrifice" OT rows mask
53
60
  * the revealed combination (receiver privacy) and give statistical soundness. This makes the OT
54
61
  * extension **malicious-secure**, not just semi-honest.
@@ -60,23 +67,38 @@
60
67
  * `u`. See docs §7.1 for the remaining boundary (real async transport, constant-time, CSPRNG).
61
68
  */
62
69
  Object.defineProperty(exports, "__esModule", { value: true });
63
- exports.OtChannel = exports.BaseOtAbort = exports.MalOtAbort = exports.SSEC = exports.KAPPA = void 0;
70
+ exports.OtChannel = exports.OtExtSender = exports.OtExtReceiver = exports.BaseOtAbort = exports.MalOtAbort = exports.SSEC = exports.KAPPA = void 0;
64
71
  exports.splitMix64 = splitMix64;
65
72
  exports.otRandom = otRandom;
73
+ exports.bindingBytes = bindingBytes;
74
+ exports.randomBinding = randomBinding;
75
+ exports.baseOtTranscriptHash = baseOtTranscriptHash;
76
+ exports.baseOtKdf = baseOtKdf;
77
+ exports.kosChallenges = kosChallenges;
78
+ exports.kosChiWords = kosChiWords;
66
79
  exports.malBaseRandomOT = malBaseRandomOT;
80
+ exports.malBaseOtSenderSetup = malBaseOtSenderSetup;
81
+ exports.malBaseOtReceiverRespond = malBaseOtReceiverRespond;
82
+ exports.malBaseOtSenderDerive = malBaseOtSenderDerive;
83
+ exports.baseOtSenderEncrypt = baseOtSenderEncrypt;
84
+ exports.baseOtReceiverDecrypt = baseOtReceiverDecrypt;
67
85
  exports.baseOT = baseOT;
68
86
  const node_crypto_1 = require("node:crypto");
69
87
  const ec_1 = require("./ec");
70
88
  const gf128_1 = require("./gf128");
89
+ const otx_1 = require("./otx");
71
90
  const csprng_1 = require("./csprng");
72
91
  /** IKNP security parameter (number of base OTs / width of the selector s). */
73
92
  exports.KAPPA = 128;
74
93
  /**
75
94
  * KOS statistical-security parameter: the number of EXTRA random check-OT rows (beyond the `κ` that
76
95
  * mask the revealed combination) appended to every extension round. Together `κ + SSEC` padding rows
77
- * give the correlation check its receiver-privacy and statistical soundness margin.
96
+ * give the correlation check its receiver-privacy and statistical soundness margin. Roy (CRYPTO 2022, ePrint
97
+ * 2022/192) shows the KOS proof is flawed and that the attacks succeed with probability about 2^-s for `s` extra
98
+ * rows; `s = 128` (raised from 64, a WIRE CHANGE: the matrix has 64 more rows per round) makes the best known attack
99
+ * about 2^-128. Not a proof.
78
100
  */
79
- exports.SSEC = 64;
101
+ exports.SSEC = 128;
80
102
  /** Raised when the KOS correlation check fails: a cheating OT-extension receiver is detected → ABORT. */
81
103
  class MalOtAbort extends Error {
82
104
  constructor(message) {
@@ -104,8 +126,9 @@ function splitMix64(seed) {
104
126
  function otRandom(next64) {
105
127
  return {
106
128
  scalar() {
107
- // Assemble 256 bits then reduce mod L; reject 0.
108
- for (;;) {
129
+ // Assemble 256 bits then reduce mod L; reject 0. Bounded: a healthy source yields 0 with probability
130
+ // ~2^-252, so a handful of consecutive zeros means a stuck-at-zero stream - fail closed, never spin.
131
+ for (let attempt = 0; attempt < 8; attempt++) {
109
132
  let acc = 0n;
110
133
  for (let i = 0; i < 4; i++)
111
134
  acc = (acc << 64n) | next64();
@@ -113,6 +136,7 @@ function otRandom(next64) {
113
136
  if (s !== 0n)
114
137
  return s;
115
138
  }
139
+ throw new Error('otRandom: entropy stream returned zero scalars repeatedly (RNG failure)');
116
140
  },
117
141
  bytes(n) {
118
142
  const out = new Uint8Array(n);
@@ -146,66 +170,100 @@ function u32(x) {
146
170
  b[3] = x & 0xff;
147
171
  return b;
148
172
  }
149
- /** Key-derivation from a group point: H("ot-kdf" ‖ index ‖ encode(P)) → SEED_BYTES bytes. */
150
- function kdfPoint(index, p) {
151
- return sha256(TAG_KDF, u32(index), (0, ec_1.encodePoint)(p)).slice(0, SEED_BYTES);
173
+ const TAG_BINDING = new TextEncoder().encode('atlas-pca-mpc/base-ot-binding');
174
+ const TAG_TRANSCRIPT = new TextEncoder().encode('atlas-pca-mpc/base-ot-transcript');
175
+ /** Canonical, length-prefixed encoding of a binding (the Schnorr context and part of every KDF input). */
176
+ function bindingBytes(b) {
177
+ if (!(b.sessionId instanceof Uint8Array) || b.sessionId.length < 8 || b.sessionId.length > 64) {
178
+ throw new RangeError('base-OT binding: sessionId must be 8..64 bytes');
179
+ }
180
+ for (const [k, v] of [['senderIndex', b.senderIndex], ['receiverIndex', b.receiverIndex]]) {
181
+ if (!Number.isInteger(v) || v < 0 || v > 0xffffffff)
182
+ throw new RangeError(`base-OT binding: ${k} must be a uint32`);
183
+ }
184
+ if (b.senderIndex === b.receiverIndex)
185
+ throw new RangeError('base-OT binding: sender and receiver must be different parties');
186
+ const round = new TextEncoder().encode(b.round);
187
+ if (round.length === 0 || round.length > 64)
188
+ throw new RangeError('base-OT binding: round label must be 1..64 bytes');
189
+ const parts = [TAG_BINDING, u32(b.sessionId.length), b.sessionId, u32(b.senderIndex), u32(b.receiverIndex), u32(round.length), round];
190
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
191
+ let o = 0;
192
+ for (const p of parts) {
193
+ out.set(p, o);
194
+ o += p.length;
195
+ }
196
+ return out;
197
+ }
198
+ /** A fresh binding for an in-process OT (both roles simulated): random session id from `rng`. */
199
+ function randomBinding(rng, round, senderIndex = 0, receiverIndex = 1) {
200
+ return { sessionId: rng.bytes(16), senderIndex, receiverIndex, round };
201
+ }
202
+ /**
203
+ * Hash of the WHOLE base-OT transcript: binding, the sender's S and every receiver point R_i, in order.
204
+ * Every pad is derived under this hash, so changing the session, S or ANY R_j changes EVERY key.
205
+ */
206
+ function baseOtTranscriptHash(binding, S, R) {
207
+ const h = (0, node_crypto_1.createHash)('sha256');
208
+ h.update(TAG_TRANSCRIPT);
209
+ h.update(bindingBytes(binding));
210
+ h.update((0, ec_1.encodePoint)(S));
211
+ h.update(u32(R.length));
212
+ for (const r of R)
213
+ h.update((0, ec_1.encodePoint)(r));
214
+ return new Uint8Array(h.digest());
215
+ }
216
+ /**
217
+ * Key derivation from a group point, Chou–Orlandi / Simplest-OT style but over the full transcript:
218
+ * `H("ot-kdf" ‖ H(transcript) ‖ index ‖ R_i ‖ P)` → SEED_BYTES bytes.
219
+ */
220
+ function baseOtKdf(transcriptHash, index, Ri, p) {
221
+ return sha256(TAG_KDF, transcriptHash, u32(index), (0, ec_1.encodePoint)(Ri), (0, ec_1.encodePoint)(p)).slice(0, SEED_BYTES);
152
222
  }
153
223
  const TAG_KDF = new TextEncoder().encode('atlas-pca-mpc/ot-kdf');
154
- const TAG_PRG = new TextEncoder().encode('atlas-pca-mpc/ot-prg');
155
- const TAG_CRH = new TextEncoder().encode('atlas-pca-mpc/ot-crh');
156
224
  const TAG_KOS = new TextEncoder().encode('atlas-pca-mpc/ot-kos');
157
225
  const TAG_COMMIT = new TextEncoder().encode('atlas-pca-mpc/ot-msg-commit');
226
+ /** Little-endian fixed-width serialization of a non-negative bigint (callers have range-checked it). */
227
+ const leBytes = otx_1.bigToBytes;
158
228
  /**
159
- * Derive the KOS row-weights χ_j ∈ GF(2^128) for one extension round, bound to the committed `u`
160
- * matrix (Fiat–Shamir). The sender and receiver both derive the SAME χ from the `u` on the wire, so a
161
- * cheating receiver cannot pick `u` after seeing χ. In the real protocol χ comes from a secure
162
- * coin-flip after the receiver commits to `u`; the transcript-hash here models that round and keeps the
163
- * check deterministic for tests. The seed expands through a fast (non-crypto) stream — fine because χ
164
- * only needs to be unpredictable-before-commit and uniform, which the hash seed provides.
229
+ * Derive the KOS row-weights χ_j ∈ GF(2^128) for one extension round (Fiat–Shamir). The sender and
230
+ * receiver both derive the SAME χ from the FULL `u` matrix on the wire, so a cheating receiver cannot
231
+ * pick `u` after seeing χ. In the real protocol χ comes from a secure coin-flip after the receiver
232
+ * commits to `u`; the transcript hash here models that round.
233
+ *
234
+ * The weights are the output of SHAKE256 (an extendable-output function, `node:crypto`) over
235
+ * `TAG ‖ channel binding ‖ round ‖ mExt ‖ every bit of every column of u`, 16 bytes per row — a vetted
236
+ * XOF seeded by the whole transcript, not a fast non-cryptographic stream. (An earlier version expanded a
237
+ * 128-bit digest with SplitMix64, and hashed only the low 128 bits of each column; both are gone.)
165
238
  */
166
- function kosChallenges(u, mExt) {
167
- const h = (0, node_crypto_1.createHash)('sha256');
168
- h.update(TAG_KOS);
169
- h.update(u32(u.length));
170
- const buf = new Uint8Array(SEED_BYTES);
171
- for (const col of u) {
172
- let v = col;
173
- for (let i = 0; i < SEED_BYTES; i++) {
174
- buf[i] = Number(v & 0xffn);
175
- v >>= 8n;
176
- }
177
- h.update(buf);
178
- }
179
- const digest = new Uint8Array(h.digest());
180
- let lo = 0n;
181
- for (let i = 0; i < 8; i++)
182
- lo |= BigInt(digest[i]) << BigInt(8 * i);
183
- let hi = 0n;
184
- for (let i = 8; i < 16; i++)
185
- hi |= BigInt(digest[i]) << BigInt(8 * (i - 8));
186
- const s0 = splitMix64(lo === 0n ? 1n : lo);
187
- const s1 = splitMix64(hi === 0n ? 0x9e3779b97f4a7c15n : hi);
239
+ function kosChallenges(binding, round, u, mExt) {
240
+ const w = kosChiWords(binding, round, u, mExt);
188
241
  const chi = new Array(mExt);
189
242
  for (let j = 0; j < mExt; j++)
190
- chi[j] = (s0() << 64n) | s1();
243
+ chi[j] = (0, otx_1.gfToBig)(w.subarray(4 * j, 4 * j + 4));
191
244
  return chi;
192
245
  }
193
- /** XOR into `acc` the χ_j for every SET bit `j` of the (≤ mExt-bit) column `col`, fast via 32-bit words. */
194
- function accumSetBits(col, chi, acc) {
195
- let v = col;
196
- let base = 0;
197
- while (v > 0n) {
198
- let w = Number(v & 0xffffffffn) >>> 0;
199
- v >>= 32n;
200
- while (w !== 0) {
201
- const b = w & -w; // lowest set bit
202
- const j = base + (31 - Math.clz32(b));
203
- acc = (0, gf128_1.gf128Add)(acc, chi[j]);
204
- w ^= b;
205
- }
206
- base += 32;
207
- }
208
- return acc;
246
+ /** The same weights as little-endian u32 words (4 per row), the form the typed kernels consume. */
247
+ function kosChiWords(binding, round, u, mExt, colBytesList) {
248
+ const colBytes = Math.ceil(mExt / 8);
249
+ const xof = (0, node_crypto_1.createHash)('shake256', { outputLength: mExt * 16 });
250
+ xof.update(TAG_KOS);
251
+ xof.update(u32(binding.length));
252
+ xof.update(binding);
253
+ xof.update(u32(round));
254
+ xof.update(u32(mExt));
255
+ xof.update(u32(u.length));
256
+ if (colBytesList)
257
+ for (const cb of colBytesList)
258
+ xof.update(cb);
259
+ else
260
+ for (const col of u)
261
+ xof.update(leBytes(col, colBytes));
262
+ const out = xof.digest();
263
+ const words = new Uint32Array(mExt * 4);
264
+ for (let i = 0; i < words.length; i++)
265
+ words[i] = out.readUInt32LE(i * 4);
266
+ return words;
209
267
  }
210
268
  /** XOR two equal-length byte arrays. */
211
269
  function xorBytes(a, b) {
@@ -214,35 +272,6 @@ function xorBytes(a, b) {
214
272
  out[i] = a[i] ^ b[i];
215
273
  return out;
216
274
  }
217
- /** PRG: expand a seed to `nbits` bits as a bigint, from block offset `startBlock` (counter mode). */
218
- function prgBits(seed, startBlock, nbits) {
219
- const nBlocks = Math.ceil(nbits / 256);
220
- let acc = 0n;
221
- let produced = 0;
222
- for (let t = 0; t < nBlocks; t++) {
223
- const block = sha256(TAG_PRG, seed, u32(startBlock + t));
224
- for (let i = 0; i < 32 && produced < nbits; i++) {
225
- acc |= BigInt(block[i]) << BigInt(produced);
226
- produced += 8;
227
- }
228
- }
229
- // Mask to exactly nbits.
230
- return acc & ((1n << BigInt(nbits)) - 1n);
231
- }
232
- /** Correlation-robust hash H(j, row) → a 64-bit mask (field messages are < 2^61). */
233
- function crh(j, row) {
234
- const rowBytes = new Uint8Array(SEED_BYTES);
235
- let v = row;
236
- for (let i = 0; i < SEED_BYTES; i++) {
237
- rowBytes[i] = Number(v & 0xffn);
238
- v >>= 8n;
239
- }
240
- const d = sha256(TAG_CRH, u32(j), rowBytes);
241
- let acc = 0n;
242
- for (let i = 0; i < 8; i++)
243
- acc |= BigInt(d[i]) << BigInt(8 * i);
244
- return acc;
245
- }
246
275
  // --------------------------------------------------------------------------------------------------
247
276
  // Base OT — MALICIOUSLY-secure Chou–Orlandi, batched (docs §7.1 item a).
248
277
  //
@@ -280,42 +309,107 @@ function ctEqual(a, b) {
280
309
  * No messages are transferred — the pads ARE the random output. `tamper` injects malicious behaviour
281
310
  * for tests. Both parties are simulated in-process.
282
311
  */
283
- function malBaseRandomOT(choices, rng = (0, csprng_1.secureOtRandom)(), tamper) {
284
- const count = choices.length;
285
- // --- Sender setup: S = y·B, T = y·S, and a Schnorr proof of knowledge of y. ---
286
- const y = rng.scalar();
287
- const S0 = (0, ec_1.mulBase)(y);
288
- const Tpt = (0, ec_1.mul)(y, S0); // T = y·S = y²·B
289
- const proof0 = (0, ec_1.schnorrProve)(y, S0, () => rng.scalar());
312
+ function malBaseRandomOT(choices, rng = (0, csprng_1.secureOtRandom)(), tamper, binding = randomBinding(rng, 'base-ot')) {
313
+ const setup = malBaseOtSenderSetup(rng, binding);
290
314
  // What actually goes on the wire (a malicious sender may tamper either).
291
- const S = tamper?.tamperS ? tamper.tamperS(S0) : S0;
292
- const proof = tamper?.tamperProof ? tamper.tamperProof(proof0) : proof0;
293
- // --- Receiver: validate the sender's point + proof BEFORE forming any R (malicious-sender guard). ---
315
+ const S = tamper?.tamperS ? tamper.tamperS(setup.S) : setup.S;
316
+ const proof = tamper?.tamperProof ? tamper.tamperProof(setup.proof) : setup.proof;
317
+ const { R, recvPads } = malBaseOtReceiverRespond(S, proof, choices, rng, binding);
318
+ // A malicious receiver may tamper the R it sends on the wire (test-injected).
319
+ if (tamper?.tamperR)
320
+ tamper.tamperR(R);
321
+ const senderPads = malBaseOtSenderDerive(setup.y, setup.T, R, S, binding);
322
+ return { S, proof, R, senderPads, recvPads };
323
+ }
324
+ // --- The same protocol as explicit per-party steps, so it can run over a real network (`net/ot-net.ts`). ---
325
+ /** SENDER step 1: S = y·B, T = y·S, and a Schnorr proof of knowledge of y. `y`/`T` stay sender-local. */
326
+ function malBaseOtSenderSetup(rng, binding) {
327
+ const ctx = bindingBytes(binding);
328
+ const y = rng.scalar();
329
+ const S = (0, ec_1.mulBase)(y);
330
+ const T = (0, ec_1.mul)(y, S); // T = y·S = y²·B
331
+ const proof = (0, ec_1.schnorrProve)(y, S, () => rng.scalar(), ctx);
332
+ return { y, T, S, proof };
333
+ }
334
+ /**
335
+ * RECEIVER step: validate the sender's point + proof BEFORE forming any R (malicious-sender guard), then
336
+ * choose x_i, form R_i = x_i·B (+ S if c_i=1) and derive recvPad_i = H(x_i·S). Throws `BaseOtAbort`/
337
+ * `PointValidationError` on a bad S or proof.
338
+ */
339
+ function malBaseOtReceiverRespond(S, proof, choices, rng, binding) {
340
+ const ctx = bindingBytes(binding);
294
341
  (0, ec_1.assertInSubgroup)(S, 'base-OT S (received by receiver)');
295
- if (!(0, ec_1.schnorrVerify)(S, proof)) {
342
+ if (!(0, ec_1.schnorrVerify)(S, proof, ctx)) {
296
343
  throw new BaseOtAbort('base OT: sender Schnorr proof-of-knowledge of S=y·B failed; aborting');
297
344
  }
298
- // --- Receiver: choose x_i, form R_i = x_i·B (+ S if c_i=1), derive recvPad_i = H(x_i·S). ---
299
345
  const R = [];
300
- const recvPads = [];
301
- for (let i = 0; i < count; i++) {
346
+ const xs = [];
347
+ for (let i = 0; i < choices.length; i++) {
302
348
  const x = rng.scalar();
303
349
  const ci = choices[i] & 1;
304
- const Ri = ci === 1 ? (0, ec_1.add)((0, ec_1.mulBase)(x), S) : (0, ec_1.mulBase)(x);
305
- R.push(Ri);
306
- recvPads.push(kdfPoint(i, (0, ec_1.mul)(x, S)));
350
+ R.push(ci === 1 ? (0, ec_1.add)((0, ec_1.mulBase)(x), S) : (0, ec_1.mulBase)(x));
351
+ xs.push(x);
307
352
  }
308
- // A malicious receiver may tamper the R it sends on the wire (test-injected).
309
- if (tamper?.tamperR)
310
- tamper.tamperR(R);
311
- // --- Sender: validate every received R_i (malicious-receiver / small-subgroup guard), then derive pads. ---
353
+ // Keys are derived under the hash of the WHOLE transcript (binding, S, every R_i).
354
+ const th = baseOtTranscriptHash(binding, S, R);
355
+ const recvPads = xs.map((x, i) => baseOtKdf(th, i, R[i], (0, ec_1.mul)(x, S)));
356
+ return { R, recvPads };
357
+ }
358
+ /** SENDER step 2: validate every received R_i (malicious-receiver / small-subgroup guard), derive both pads. */
359
+ function malBaseOtSenderDerive(y, T, R, S, binding) {
312
360
  const senderPads = [];
313
- for (let i = 0; i < count; i++) {
361
+ for (let i = 0; i < R.length; i++)
314
362
  (0, ec_1.assertInSubgroup)(R[i], `base-OT R_${i} (received by sender)`);
363
+ const th = baseOtTranscriptHash(binding, S, R);
364
+ for (let i = 0; i < R.length; i++) {
315
365
  const yR = (0, ec_1.mul)(y, R[i]);
316
- senderPads.push([kdfPoint(i, yR), kdfPoint(i, (0, ec_1.sub)(yR, Tpt))]);
366
+ senderPads.push([baseOtKdf(th, i, R[i], yR), baseOtKdf(th, i, R[i], (0, ec_1.sub)(yR, T))]);
317
367
  }
318
- return { S, proof, R, senderPads, recvPads };
368
+ return senderPads;
369
+ }
370
+ /** SENDER step 3 (chosen-message wrapper): one-time-pad each message under its pad and commit to it. */
371
+ function baseOtSenderEncrypt(messages, senderPads) {
372
+ if (messages.length !== senderPads.length)
373
+ throw new Error('baseOT: messages/pads length mismatch');
374
+ for (const [i, [m0, m1]] of messages.entries()) {
375
+ if (m0.length > SEED_BYTES || m1.length > SEED_BYTES || m0.length !== m1.length) {
376
+ throw new RangeError(`baseOT: message pair ${i} must be equal length and at most ${SEED_BYTES} bytes (pad width)`);
377
+ }
378
+ }
379
+ const ct = [];
380
+ const com = [];
381
+ for (let i = 0; i < messages.length; i++) {
382
+ const [p0, p1] = senderPads[i];
383
+ ct.push([xorBytes(messages[i][0], p0), xorBytes(messages[i][1], p1)]);
384
+ com.push([otCommit(i, 0, messages[i][0], p0), otCommit(i, 1, messages[i][1], p1)]);
385
+ }
386
+ return { ct, com };
387
+ }
388
+ /**
389
+ * RECEIVER step 3: decrypt the chosen ciphertext with its pad, then CHECK the commitment it can open;
390
+ * `BaseOtAbort` on any inconsistency (a cheating sender). Every ciphertext/commitment must have the shape
391
+ * the receiver expects (equal-length byte strings of at most the pad width).
392
+ */
393
+ function baseOtReceiverDecrypt(choices, recvPads, ct, com) {
394
+ if (ct.length !== choices.length || com.length !== choices.length || recvPads.length !== choices.length) {
395
+ throw new BaseOtAbort('base OT: ciphertext/commitment count does not match the number of choices; aborting');
396
+ }
397
+ const received = [];
398
+ for (let i = 0; i < choices.length; i++) {
399
+ const ci = choices[i] & 1;
400
+ const c0 = ct[i][0];
401
+ const c1 = ct[i][1];
402
+ if (c0.length !== c1.length || c0.length > SEED_BYTES) {
403
+ throw new BaseOtAbort(`base OT (index ${i}): malformed ciphertext shape; aborting`);
404
+ }
405
+ const m = xorBytes(ct[i][ci], recvPads[i]);
406
+ const check = otCommit(i, ci, m, recvPads[i]);
407
+ if (!ctEqual(check, com[i][ci])) {
408
+ throw new BaseOtAbort(`base OT (index ${i}): sender ciphertext inconsistent with its commitment; aborting`);
409
+ }
410
+ received.push(m);
411
+ }
412
+ return received;
319
413
  }
320
414
  /**
321
415
  * Run `count` MALICIOUS-secure base OTs in one batch (chosen-message, over the random OT above).
@@ -324,58 +418,275 @@ function malBaseRandomOT(choices, rng = (0, csprng_1.secureOtRandom)(), tamper)
324
418
  * the receiver recovers `m_{i,c_i}` and checks the commitment, aborting (`BaseOtAbort`) on any
325
419
  * inconsistency. Returns the receiver's recovered messages and the full transcript.
326
420
  */
327
- function baseOT(messages, choices, rng = (0, csprng_1.secureOtRandom)(), tamper) {
421
+ function baseOT(messages, choices, rng = (0, csprng_1.secureOtRandom)(), tamper, binding = randomBinding(rng, 'base-ot')) {
328
422
  if (messages.length !== choices.length)
329
423
  throw new Error('baseOT: messages/choices length mismatch');
330
- const count = messages.length;
331
- const ro = malBaseRandomOT(choices, rng, tamper);
332
- // Sender: E_{i,b} = m_{i,b} ⊕ pad_{i,b}, and commit com_{i,b} = H(i ‖ b ‖ m_{i,b} ‖ pad_{i,b}).
333
- const ct = [];
334
- const com = [];
335
- for (let i = 0; i < count; i++) {
336
- const [p0, p1] = ro.senderPads[i];
337
- ct.push([xorBytes(messages[i][0], p0), xorBytes(messages[i][1], p1)]);
338
- com.push([otCommit(i, 0, messages[i][0], p0), otCommit(i, 1, messages[i][1], p1)]);
339
- }
424
+ // The one-time pads are SEED_BYTES wide: a longer message would have its tail sent in the clear.
425
+ const ro = malBaseRandomOT(choices, rng, tamper, binding);
426
+ const { ct, com } = baseOtSenderEncrypt(messages, ro.senderPads);
340
427
  // A malicious sender may corrupt the ciphertexts on the wire (inconsistent with the commitment).
341
428
  if (tamper?.tamperCiphertext)
342
429
  tamper.tamperCiphertext(ct);
343
- // Receiver: decrypt the chosen ciphertext with its pad, then CHECK the commitment it can open.
344
- const received = [];
345
- for (let i = 0; i < count; i++) {
346
- const ci = choices[i] & 1;
347
- const m = xorBytes(ct[i][ci], ro.recvPads[i]);
348
- const check = otCommit(i, ci, m, ro.recvPads[i]);
349
- if (!ctEqual(check, com[i][ci])) {
350
- throw new BaseOtAbort(`base OT (index ${i}): sender ciphertext inconsistent with its commitment; aborting`);
430
+ const received = baseOtReceiverDecrypt(choices, ro.recvPads, ct, com);
431
+ return { received, transcript: { S: ro.S, proof: ro.proof, R: ro.R, ct, com } };
432
+ }
433
+ function checkExtInputs(choiceBits, msg0, msg1) {
434
+ const m = choiceBits.length;
435
+ const MSG_LIMIT = 1n << 64n;
436
+ for (let j = 0; j < m; j++) {
437
+ const c = choiceBits[j];
438
+ if (c !== 0 && c !== 1)
439
+ throw new RangeError(`OtChannel.extend: choice bit ${j} must be 0 or 1`);
440
+ }
441
+ if (msg0 && msg1) {
442
+ for (let j = 0; j < m; j++) {
443
+ const a = msg0[j];
444
+ const b = msg1[j];
445
+ if (typeof a !== 'bigint' || typeof b !== 'bigint' || a < 0n || b < 0n || a >= MSG_LIMIT || b >= MSG_LIMIT) {
446
+ throw new RangeError(`OtChannel.extend: message ${j} must be a bigint in [0, 2^64) (the pad is 64 bits wide)`);
447
+ }
351
448
  }
352
- received.push(m);
353
449
  }
354
- return { received, transcript: { S: ro.S, proof: ro.proof, R: ro.R, ct, com } };
355
450
  }
356
- // --------------------------------------------------------------------------------------------------
357
- // OT extension — IKNP (semi-honest), as a stateful in-process channel for one (receiver, sender) pair.
358
- // --------------------------------------------------------------------------------------------------
359
451
  /**
360
- * An IKNP OT-extension channel between a fixed RECEIVER (choice bits) and SENDER (message pairs). The
361
- * κ base OTs run lazily on the first `extend`; each later `extend(m)` is a cheap PRG+hash round. The
362
- * channel is stateful (advances per-seed PRG counters) so many rounds compose into one amortized setup.
452
+ * The OT-extension RECEIVER (holds the choice bits). In the base OT it is the SENDER: it draws κ seed pairs
453
+ * and offers them; after the base OT it expands them with the PRG.
454
+ */
455
+ class OtExtReceiver {
456
+ rng;
457
+ kosBinding;
458
+ seed0 = [];
459
+ seed1 = [];
460
+ blk0 = [];
461
+ blk1 = [];
462
+ round = 0;
463
+ /** Rows hashed in earlier rounds (R4-12 global tweak offset); advanced by m per round, in step with the sender. */
464
+ rowsDone = 0;
465
+ tweakDomain;
466
+ ready = false;
467
+ /**
468
+ * TEST-ONLY: called with the honest `u` matrix just before it goes on the wire; may mutate it (a malicious
469
+ * receiver encoding inconsistent choices). The KOS values are computed over the tampered `u`.
470
+ */
471
+ tamperU;
472
+ constructor(rng, kosBinding) {
473
+ this.rng = rng;
474
+ this.kosBinding = kosBinding;
475
+ this.tweakDomain = (0, otx_1.otTweakDomain)(kosBinding);
476
+ }
477
+ /** Base-OT messages: the κ seed pairs this party offers as the base-OT sender. */
478
+ makeSeeds() {
479
+ const messages = [];
480
+ for (let i = 0; i < exports.KAPPA; i++) {
481
+ const k0 = this.rng.bytes(SEED_BYTES);
482
+ const k1 = this.rng.bytes(SEED_BYTES);
483
+ this.seed0.push(k0);
484
+ this.seed1.push(k1);
485
+ this.blk0.push(0);
486
+ this.blk1.push(0);
487
+ messages.push([k0, k1]);
488
+ }
489
+ this.ready = true;
490
+ return messages;
491
+ }
492
+ /** Round start: form u and the KOS pair for `choiceBits` (the real rows); returns the wire message + pending state. */
493
+ begin(choiceBits) {
494
+ if (!this.ready)
495
+ throw new Error('OtExtReceiver: base OT not done');
496
+ checkExtInputs(choiceBits, undefined, undefined);
497
+ const m = choiceBits.length;
498
+ const roundIdx = this.round++;
499
+ const rowOffset = this.rowsDone;
500
+ this.rowsDone += m;
501
+ const pad = exports.KAPPA + exports.SSEC;
502
+ const mExt = m + pad;
503
+ const W = (mExt + 31) >>> 5;
504
+ const colBytes = Math.ceil(mExt / 8);
505
+ const bvec = new Uint32Array(W);
506
+ for (let j = 0; j < m; j++)
507
+ if (choiceBits[j] & 1)
508
+ bvec[j >>> 5] = (bvec[j >>> 5] | (1 << (j & 31))) >>> 0;
509
+ const padBytes = this.rng.bytes(pad);
510
+ for (let p = 0; p < pad; p++) {
511
+ if (padBytes[p] & 1) {
512
+ const j = m + p;
513
+ bvec[j >>> 5] = (bvec[j >>> 5] | (1 << (j & 31))) >>> 0;
514
+ }
515
+ }
516
+ const blocks = Math.ceil(W / 4); // 16-byte AES blocks consumed per column
517
+ const t = new Uint32Array(otx_1.COLS * W);
518
+ const u = new Array(otx_1.COLS);
519
+ const uw = new Uint32Array(W);
520
+ const ubytes = new Array(otx_1.COLS);
521
+ for (let i = 0; i < exports.KAPPA; i++) {
522
+ const ti = (0, otx_1.prgColumn)(this.seed0[i], this.blk0[i], W);
523
+ const gi = (0, otx_1.prgColumn)(this.seed1[i], this.blk1[i], W);
524
+ this.blk0[i] += blocks;
525
+ this.blk1[i] += blocks;
526
+ (0, otx_1.maskTail)(ti, mExt);
527
+ (0, otx_1.maskTail)(gi, mExt);
528
+ t.set(ti, i * W);
529
+ for (let w = 0; w < W; w++)
530
+ uw[w] = (gi[w] ^ ti[w] ^ bvec[w]) >>> 0;
531
+ ubytes[i] = new Uint8Array(uw.buffer.slice(0, colBytes));
532
+ u[i] = (0, otx_1.bytesToBig)(ubytes[i]);
533
+ }
534
+ if (this.tamperU)
535
+ this.tamperU(u, roundIdx);
536
+ const pending = { m, choiceBits: choiceBits.slice(), t, W, rowOffset };
537
+ // A (test-injected) malformed matrix cannot be hashed; send it as is with a zero pair - the sender's
538
+ // structural check aborts on it before any arithmetic.
539
+ const limit = 1n << BigInt(mExt);
540
+ if (u.some((c) => typeof c !== 'bigint' || c < 0n || c >= limit)) {
541
+ return { msg: { u, x: 0n, tComb: 0n }, pending };
542
+ }
543
+ const chi = kosChiWords(this.kosBinding, roundIdx, u, mExt, this.tamperU ? undefined : ubytes);
544
+ const x = new Uint32Array(4);
545
+ (0, otx_1.accumColumn)(bvec, chi, x);
546
+ const tComb = (0, otx_1.kosCombine)(t, W, chi);
547
+ return { msg: { u, x: (0, otx_1.gfToBig)(x), tComb: (0, otx_1.gfToBig)(tComb) }, pending };
548
+ }
549
+ /** Round end: recover the chosen messages from the sender's ciphertexts. */
550
+ finish(pending, y0, y1) {
551
+ const { m, choiceBits, t, W, rowOffset } = pending;
552
+ if (y0.length !== m || y1.length !== m)
553
+ throw new MalOtAbort('OT extension: ciphertext count does not match the number of rows; aborting');
554
+ const rows = (0, otx_1.transposeCols)(t, W, m);
555
+ const h = (0, otx_1.tmmoCrh)(rows, m, { offset: rowOffset, domain: this.tweakDomain });
556
+ const received = new Array(m);
557
+ for (let j = 0; j < m; j++)
558
+ received[j] = (choiceBits[j] === 1 ? y1[j] : y0[j]) ^ (0, otx_1.word64)(h, j);
559
+ return received;
560
+ }
561
+ }
562
+ exports.OtExtReceiver = OtExtReceiver;
563
+ /** The OT-extension SENDER (holds the message pairs). In the base OT it is the RECEIVER with selector `s`. */
564
+ class OtExtSender {
565
+ rng;
566
+ kosBinding;
567
+ s = [];
568
+ sBits = 0n;
569
+ seeds = [];
570
+ blk = [];
571
+ round = 0;
572
+ rowsDone = 0;
573
+ tweakDomain;
574
+ ready = false;
575
+ constructor(rng, kosBinding) {
576
+ this.rng = rng;
577
+ this.kosBinding = kosBinding;
578
+ this.tweakDomain = (0, otx_1.otTweakDomain)(kosBinding);
579
+ }
580
+ /** Draw the secret selector s ∈ {0,1}^κ (the base-OT choice bits). */
581
+ makeSelector() {
582
+ this.s = Array.from({ length: exports.KAPPA }, () => this.rng.bytes(1)[0] & 1);
583
+ return this.s.slice();
584
+ }
585
+ /** Install the κ seeds learned from the base OT. */
586
+ setSeeds(seeds) {
587
+ if (seeds.length !== exports.KAPPA || this.s.length !== exports.KAPPA)
588
+ throw new Error('OtExtSender: wrong base-OT output size');
589
+ this.seeds = seeds;
590
+ this.blk = new Array(exports.KAPPA).fill(0);
591
+ let acc = 0n;
592
+ for (let i = 0; i < exports.KAPPA; i++)
593
+ acc |= BigInt(this.s[i]) << BigInt(i);
594
+ this.sBits = acc;
595
+ this.ready = true;
596
+ }
597
+ get selector() {
598
+ return this.sBits;
599
+ }
600
+ /**
601
+ * Verify the receiver's round message (structure + KOS correlation check) and return the ciphertexts for
602
+ * the `m` real rows. A cheating receiver is caught here: `MalOtAbort`.
603
+ */
604
+ respond(msg, msg0, msg1) {
605
+ if (!this.ready)
606
+ throw new Error('OtExtSender: base OT not done');
607
+ const m = msg0.length;
608
+ if (msg1.length !== m)
609
+ throw new Error('OtChannel.extend: length mismatch');
610
+ checkExtInputs(new Array(m).fill(0), msg0, msg1);
611
+ const roundIdx = this.round++;
612
+ const rowOffset = this.rowsDone;
613
+ this.rowsDone += m;
614
+ const mExt = m + exports.KAPPA + exports.SSEC;
615
+ const u = msg.u;
616
+ if (!Array.isArray(u) || u.length !== exports.KAPPA) {
617
+ throw new MalOtAbort(`KOS check (round ${roundIdx}): wrong number of u columns; a cheating OT-extension receiver was detected; aborting`);
618
+ }
619
+ // Structural check on what arrived: every column must be an mExt-bit non-negative integer. Bits beyond
620
+ // mExt have no challenge weight, and would otherwise crash the correlation check with an untyped error.
621
+ const colLimit = 1n << BigInt(mExt);
622
+ for (let i = 0; i < exports.KAPPA; i++) {
623
+ const ui = u[i];
624
+ if (typeof ui !== 'bigint' || ui < 0n || ui >= colLimit) {
625
+ throw new MalOtAbort(`KOS check (round ${roundIdx}): malformed u column ${i}; a cheating OT-extension receiver was detected; aborting`);
626
+ }
627
+ }
628
+ const lim128 = 1n << 128n;
629
+ if (typeof msg.x !== 'bigint' || typeof msg.tComb !== 'bigint' || msg.x < 0n || msg.x >= lim128 || msg.tComb < 0n || msg.tComb >= lim128) {
630
+ throw new MalOtAbort(`KOS check (round ${roundIdx}): malformed (x, t); a cheating OT-extension receiver was detected; aborting`);
631
+ }
632
+ const W = (mExt + 31) >>> 5;
633
+ const colBytes = Math.ceil(mExt / 8);
634
+ const blocks = Math.ceil(W / 4);
635
+ // q_i = (s_i ? u_i : 0) ⊕ PRG(k_i^{s_i}) => q_i = t_i ⊕ (s_i · bvec) if the receiver is honest.
636
+ const q = new Uint32Array(otx_1.COLS * W);
637
+ const ubytes = new Array(otx_1.COLS);
638
+ for (let i = 0; i < exports.KAPPA; i++) {
639
+ ubytes[i] = (0, otx_1.bigToBytes)(u[i], colBytes);
640
+ const ks = (0, otx_1.prgColumn)(this.seeds[i], this.blk[i], W);
641
+ this.blk[i] += blocks;
642
+ (0, otx_1.maskTail)(ks, mExt);
643
+ if (this.s[i] === 1) {
644
+ const ub = new Uint8Array(4 * W);
645
+ ub.set(ubytes[i]);
646
+ const uw = new Uint32Array(ub.buffer, 0, W);
647
+ for (let w = 0; w < W; w++)
648
+ ks[w] = (ks[w] ^ uw[w]) >>> 0;
649
+ }
650
+ q.set(ks, i * W);
651
+ }
652
+ // KOS correlation check (GF(2^128)): Σ χ_j·q_j == t ⊕ x·s with χ bound to the whole u on the wire.
653
+ const chi = kosChiWords(this.kosBinding, roundIdx, u, mExt, ubytes);
654
+ const qComb = (0, otx_1.gfToBig)((0, otx_1.kosCombine)(q, W, chi));
655
+ const expected = msg.tComb ^ (0, gf128_1.gf128Mul)(msg.x, this.sBits);
656
+ if (qComb !== expected) {
657
+ throw new MalOtAbort(`KOS correlation check FAILED (round ${roundIdx}): a cheating OT-extension receiver was detected; aborting`);
658
+ }
659
+ const rows = (0, otx_1.transposeCols)(q, W, m);
660
+ const tweak = { offset: rowOffset, domain: this.tweakDomain };
661
+ const h0 = (0, otx_1.tmmoCrh)(rows, m, tweak);
662
+ const sw = (0, otx_1.gfFromBig)(this.sBits);
663
+ for (let j = 0; j < m; j++)
664
+ (0, otx_1.gfXorInto)(rows.subarray(4 * j, 4 * j + 4), sw, 0);
665
+ const h1 = (0, otx_1.tmmoCrh)(rows, m, tweak);
666
+ const y0 = new Array(m);
667
+ const y1 = new Array(m);
668
+ for (let j = 0; j < m; j++) {
669
+ y0[j] = msg0[j] ^ (0, otx_1.word64)(h0, j);
670
+ y1[j] = msg1[j] ^ (0, otx_1.word64)(h1, j);
671
+ }
672
+ return { y0, y1 };
673
+ }
674
+ }
675
+ exports.OtExtSender = OtExtSender;
676
+ /**
677
+ * An IKNP/KOS OT-extension channel between a fixed RECEIVER (choice bits) and SENDER (message pairs), both
678
+ * simulated in-process. The κ base OTs run lazily on the first `extend`; each later `extend(m)` is a cheap
679
+ * PRG+hash round. The channel is stateful (advances per-seed PRG counters) so many rounds compose into one
680
+ * amortized setup. The two roles are the standalone `OtExtReceiver` / `OtExtSender` (used over the network).
363
681
  */
364
682
  class OtChannel {
365
683
  rng;
366
684
  /** Optional post-quantum (or otherwise custom) base OT for the κ setup OTs; defaults to EC `baseOT`. */
367
685
  baseOtFn;
686
+ bindingOpt;
687
+ recv;
688
+ send;
368
689
  ready = false;
369
- // Extension-sender state: selector s (κ bits) and the κ seeds it learned (k_i^{s_i}).
370
- s = [];
371
- sBits = 0n; // s packed as a κ-bit bigint (cached)
372
- senderSeeds = [];
373
- senderBlk = [];
374
- // Extension-receiver state: κ seed pairs.
375
- recvSeed0 = [];
376
- recvSeed1 = [];
377
- recvBlk0 = [];
378
- recvBlk1 = [];
379
690
  /** The base-OT transcript (available after setup) — exposed for inspection/tests. */
380
691
  baseTranscript;
381
692
  /**
@@ -386,7 +697,6 @@ class OtChannel {
386
697
  * a non-trivial tamper is caught — the real malicious-receiver attack that KOS defeats.
387
698
  */
388
699
  tamperU;
389
- round = 0;
390
700
  /**
391
701
  * `rng` defaults to the CSPRNG (`secureOtRandom`, `node:crypto`) — production randomness. Pass a
392
702
  * deterministic `otRandom(splitMix64(seed))` ONLY to make a test bit-reproducible (docs §7.1 item c).
@@ -394,46 +704,37 @@ class OtChannel {
394
704
  constructor(rng = (0, csprng_1.secureOtRandom)(), opts) {
395
705
  this.rng = rng;
396
706
  this.baseOtFn = opts?.baseOtFn;
707
+ this.bindingOpt = opts?.binding;
397
708
  }
398
709
  setup() {
399
- // Extension-RECEIVER is the base-SENDER: it generates κ seed pairs.
400
- const messages = [];
401
- for (let i = 0; i < exports.KAPPA; i++) {
402
- const k0 = this.rng.bytes(SEED_BYTES);
403
- const k1 = this.rng.bytes(SEED_BYTES);
404
- this.recvSeed0.push(k0);
405
- this.recvSeed1.push(k1);
406
- this.recvBlk0.push(0);
407
- this.recvBlk1.push(0);
408
- messages.push([k0, k1]);
409
- }
410
- // Extension-SENDER is the base-RECEIVER with selector s: it learns k_i^{s_i}.
411
- this.s = Array.from({ length: exports.KAPPA }, () => {
412
- const b = this.rng.bytes(1);
413
- return b[0] & 1;
414
- });
710
+ const base = this.bindingOpt ?? randomBinding(this.rng, 'x');
711
+ const binding = { sessionId: base.sessionId, senderIndex: base.senderIndex, receiverIndex: base.receiverIndex };
712
+ const kos = bindingBytes({ ...binding, round: 'iknp-kos' });
713
+ const recv = new OtExtReceiver(this.rng, kos);
714
+ const send = new OtExtSender(this.rng, kos);
715
+ // Extension-RECEIVER is the base-SENDER: it generates κ seed pairs; the extension-SENDER is the
716
+ // base-RECEIVER with selector s: it learns k_i^{s_i}.
717
+ const messages = recv.makeSeeds();
718
+ const s = send.makeSelector();
415
719
  if (this.baseOtFn) {
416
720
  // Custom (e.g. post-quantum ML-KEM) base OT: its transcript shape differs, so leave the
417
721
  // EC-typed `baseTranscript` unset.
418
- this.senderSeeds = this.baseOtFn(messages, this.s, this.rng).received;
722
+ send.setSeeds(this.baseOtFn(messages, s, this.rng).received);
419
723
  }
420
724
  else {
421
- const base = baseOT(messages, this.s, this.rng);
422
- this.senderSeeds = base.received;
423
- this.baseTranscript = base.transcript;
725
+ const baseRes = baseOT(messages, s, this.rng, undefined, { ...binding, round: 'iknp-kappa-setup' });
726
+ send.setSeeds(baseRes.received);
727
+ this.baseTranscript = baseRes.transcript;
424
728
  }
425
- this.senderBlk = new Array(exports.KAPPA).fill(0);
426
- let acc = 0n;
427
- for (let i = 0; i < exports.KAPPA; i++)
428
- acc |= BigInt(this.s[i]) << BigInt(i);
429
- this.sBits = acc;
729
+ this.recv = recv;
730
+ this.send = send;
430
731
  this.ready = true;
431
732
  }
432
733
  /** Selector s as a κ-bit bigint (sender-side secret; exposed only for tests). */
433
734
  get selector() {
434
735
  if (!this.ready)
435
736
  this.setup();
436
- return this.sBits;
737
+ return this.send.selector;
437
738
  }
438
739
  /**
439
740
  * One MALICIOUS-secure extension round: `choiceBits[j] ∈ {0,1}` (receiver), `msg0[j]/msg1[j]`
@@ -451,85 +752,11 @@ class OtChannel {
451
752
  throw new Error('OtChannel.extend: length mismatch');
452
753
  if (m === 0)
453
754
  return { received: [], uMatrix: [] };
454
- const roundIdx = this.round++;
455
- // --- KOS padding: append κ+SSEC random check-OT rows (no messages are transferred for them). ---
456
- const pad = exports.KAPPA + exports.SSEC;
457
- const mExt = m + pad;
458
- const padChoice = new Array(pad);
459
- for (let p = 0; p < pad; p++)
460
- padChoice[p] = this.rng.bytes(1)[0] & 1;
461
- // Full choice vector (real ‖ padding) as an mExt-bit bigint, and a per-row accessor for the check.
462
- let bvec = 0n;
463
- for (let j = 0; j < m; j++)
464
- if (choiceBits[j] & 1)
465
- bvec |= 1n << BigInt(j);
466
- for (let p = 0; p < pad; p++)
467
- if (padChoice[p] & 1)
468
- bvec |= 1n << BigInt(m + p);
469
- const blocks = Math.ceil(mExt / 256);
470
- // Receiver forms t_i, u_i = PRG(k1) ⊕ t_i ⊕ bvec (over all mExt rows).
471
- const t = new Array(exports.KAPPA);
472
- const u = new Array(exports.KAPPA);
473
- for (let i = 0; i < exports.KAPPA; i++) {
474
- const ti = prgBits(this.recvSeed0[i], this.recvBlk0[i], mExt);
475
- const gi = prgBits(this.recvSeed1[i], this.recvBlk1[i], mExt);
476
- this.recvBlk0[i] += blocks;
477
- this.recvBlk1[i] += blocks;
478
- t[i] = ti;
479
- u[i] = gi ^ ti ^ bvec;
480
- }
481
- // A malicious receiver may tamper the u it sends on the wire (test-injected).
482
- if (this.tamperU)
483
- this.tamperU(u, roundIdx);
484
- // Sender computes q_i = (s_i ? u_i : 0) ⊕ PRG(k_i^{s_i}). => q_i = t_i ⊕ (s_i · bvec) if honest.
485
- const q = new Array(exports.KAPPA);
486
- for (let i = 0; i < exports.KAPPA; i++) {
487
- const ks = prgBits(this.senderSeeds[i], this.senderBlk[i], mExt);
488
- this.senderBlk[i] += blocks;
489
- q[i] = (this.s[i] === 1 ? u[i] : 0n) ^ ks;
490
- }
491
- const sBits = this.sBits;
492
- // --- KOS correlation check (GF(2^128), column-domain fold over all mExt rows). ---
493
- // χ_j bound to the (possibly tampered) u on the wire. Receiver reveals x = Σ b_j·χ_j and
494
- // t_comb = Σ χ_j·t_j; sender checks Σ χ_j·q_j == t_comb ⊕ x·s. Honestly q_j = t_j ⊕ b_j·s so it
495
- // holds; a receiver who tampered u passes with prob ≤ 2^{-128}, else ABORT.
496
- const chi = kosChallenges(u, mExt);
497
- // x = Σ_{j: b_j=1} χ_j (receiver side; over real + padding choices, so real choices stay masked).
498
- let x = 0n;
499
- x = accumSetBits(bvec, chi, x);
500
- // t_comb = Σ_i (Σ_{j: t_i has bit j} χ_j)·x^i ; q_comb likewise from q_i.
501
- let tComb = 0n;
502
- let qComb = 0n;
503
- for (let i = 0; i < exports.KAPPA; i++) {
504
- const at = accumSetBits(t[i], chi, 0n);
505
- const aq = accumSetBits(q[i], chi, 0n);
506
- tComb = (0, gf128_1.gf128Add)(tComb, (0, gf128_1.gf128MulByXi)(at, i));
507
- qComb = (0, gf128_1.gf128Add)(qComb, (0, gf128_1.gf128MulByXi)(aq, i));
508
- }
509
- const expected = (0, gf128_1.gf128Add)(tComb, (0, gf128_1.gf128Mul)(x, sBits));
510
- if (qComb !== expected) {
511
- throw new MalOtAbort(`KOS correlation check FAILED (round ${roundIdx}): a cheating OT-extension receiver was detected; aborting`);
512
- }
513
- // --- Only the m real rows carry messages. Sender: y0_j = msg0_j ⊕ H(j,qrow_j), y1 ⊕ H(j,qrow⊕s). ---
514
- const y0 = new Array(m);
515
- const y1 = new Array(m);
516
- for (let j = 0; j < m; j++) {
517
- let qrow = 0n;
518
- for (let i = 0; i < exports.KAPPA; i++)
519
- qrow |= ((q[i] >> BigInt(j)) & 1n) << BigInt(i);
520
- y0[j] = msg0[j] ^ crh(j, qrow);
521
- y1[j] = msg1[j] ^ crh(j, qrow ^ sBits);
522
- }
523
- // Receiver: recovered_j = y_{b_j}[j] ⊕ H(j, trow_j).
524
- const received = new Array(m);
525
- for (let j = 0; j < m; j++) {
526
- let trow = 0n;
527
- for (let i = 0; i < exports.KAPPA; i++)
528
- trow |= ((t[i] >> BigInt(j)) & 1n) << BigInt(i);
529
- const bj = choiceBits[j] & 1;
530
- received[j] = (bj === 1 ? y1[j] : y0[j]) ^ crh(j, trow);
531
- }
532
- return { received, uMatrix: u };
755
+ checkExtInputs(choiceBits, msg0, msg1);
756
+ this.recv.tamperU = this.tamperU;
757
+ const { msg, pending } = this.recv.begin(choiceBits);
758
+ const { y0, y1 } = this.send.respond(msg, msg0, msg1);
759
+ return { received: this.recv.finish(pending, y0, y1), uMatrix: msg.u };
533
760
  }
534
761
  }
535
762
  exports.OtChannel = OtChannel;