@atlasauth/pca-mpc 0.3.0 → 0.4.1
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.
- package/README.md +81 -3
- package/dist/beaver.js +4 -0
- package/dist/beaver.js.map +1 -1
- package/dist/compose.d.ts +9 -2
- package/dist/compose.js +24 -1
- package/dist/compose.js.map +1 -1
- package/dist/ec.d.ts +6 -3
- package/dist/ec.js +27 -9
- package/dist/ec.js.map +1 -1
- package/dist/enc.d.ts +46 -0
- package/dist/enc.js +88 -0
- package/dist/enc.js.map +1 -0
- package/dist/endemic-ot.js +24 -1
- package/dist/endemic-ot.js.map +1 -1
- package/dist/hybrid-ot.js +3 -0
- package/dist/hybrid-ot.js.map +1 -1
- package/dist/kem-ot.js +2 -0
- package/dist/kem-ot.js.map +1 -1
- package/dist/mascot.d.ts +81 -30
- package/dist/mascot.js +232 -113
- package/dist/mascot.js.map +1 -1
- package/dist/net/certs.d.ts +14 -0
- package/dist/net/certs.js +30 -0
- package/dist/net/certs.js.map +1 -0
- package/dist/net/faults.d.ts +49 -0
- package/dist/net/faults.js +222 -0
- package/dist/net/faults.js.map +1 -0
- package/dist/net/forensics.d.ts +81 -0
- package/dist/net/forensics.js +181 -0
- package/dist/net/forensics.js.map +1 -0
- package/dist/net/gadget.d.ts +44 -0
- package/dist/net/gadget.js +102 -0
- package/dist/net/gadget.js.map +1 -0
- package/dist/net/index.d.ts +11 -0
- package/dist/net/index.js +28 -0
- package/dist/net/index.js.map +1 -0
- package/dist/net/launch-local.d.ts +53 -0
- package/dist/net/launch-local.js +190 -0
- package/dist/net/launch-local.js.map +1 -0
- package/dist/net/launch-support.d.ts +6 -0
- package/dist/net/launch-support.js +65 -0
- package/dist/net/launch-support.js.map +1 -0
- package/dist/net/mesh.d.ts +135 -0
- package/dist/net/mesh.js +491 -0
- package/dist/net/mesh.js.map +1 -0
- package/dist/net/offline-net.d.ts +58 -0
- package/dist/net/offline-net.js +485 -0
- package/dist/net/offline-net.js.map +1 -0
- package/dist/net/ot-net.d.ts +42 -0
- package/dist/net/ot-net.js +219 -0
- package/dist/net/ot-net.js.map +1 -0
- package/dist/net/pa.d.ts +65 -0
- package/dist/net/pa.js +118 -0
- package/dist/net/pa.js.map +1 -0
- package/dist/net/party-main.d.ts +48 -0
- package/dist/net/party-main.js +146 -0
- package/dist/net/party-main.js.map +1 -0
- package/dist/net/prep.d.ts +40 -0
- package/dist/net/prep.js +12 -0
- package/dist/net/prep.js.map +1 -0
- package/dist/net/protocol.d.ts +126 -0
- package/dist/net/protocol.js +655 -0
- package/dist/net/protocol.js.map +1 -0
- package/dist/net/wire.d.ts +89 -0
- package/dist/net/wire.js +242 -0
- package/dist/net/wire.js.map +1 -0
- package/dist/ot.d.ts +176 -17
- package/dist/ot.js +479 -252
- package/dist/ot.js.map +1 -1
- package/dist/otx.d.ts +76 -0
- package/dist/otx.js +238 -0
- package/dist/otx.js.map +1 -0
- package/dist/runner.d.ts +1 -1
- package/dist/runner.js +6 -1
- package/dist/runner.js.map +1 -1
- package/dist/sharing.d.ts +2 -0
- package/dist/sharing.js +7 -4
- package/dist/sharing.js.map +1 -1
- package/dist/spdz-runner.d.ts +20 -3
- package/dist/spdz-runner.js +21 -3
- package/dist/spdz-runner.js.map +1 -1
- package/dist/spdz.d.ts +96 -22
- package/dist/spdz.js +196 -47
- package/dist/spdz.js.map +1 -1
- 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`
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
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
|
|
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] = (
|
|
243
|
+
chi[j] = (0, otx_1.gfToBig)(w.subarray(4 * j, 4 * j + 4));
|
|
191
244
|
return chi;
|
|
192
245
|
}
|
|
193
|
-
/**
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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(
|
|
292
|
-
const proof = tamper?.tamperProof ? tamper.tamperProof(
|
|
293
|
-
|
|
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
|
|
301
|
-
for (let i = 0; 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
|
-
|
|
305
|
-
|
|
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
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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 <
|
|
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([
|
|
366
|
+
senderPads.push([baseOtKdf(th, i, R[i], yR), baseOtKdf(th, i, R[i], (0, ec_1.sub)(yR, T))]);
|
|
317
367
|
}
|
|
318
|
-
return
|
|
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
|
-
|
|
331
|
-
const ro = malBaseRandomOT(choices, rng, tamper);
|
|
332
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
*
|
|
361
|
-
*
|
|
362
|
-
|
|
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
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
722
|
+
send.setSeeds(this.baseOtFn(messages, s, this.rng).received);
|
|
419
723
|
}
|
|
420
724
|
else {
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
this.baseTranscript =
|
|
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.
|
|
426
|
-
|
|
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.
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
|
|
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;
|