threshold-elgamal 1.0.5 → 1.0.8

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 (63) hide show
  1. package/README.md +3 -2
  2. package/dist/core/index.d.ts +1 -1
  3. package/dist/core/index.js +1 -1
  4. package/dist/dkg/pedersen-share-codec.js +1 -1
  5. package/dist/dkg/verification.d.ts +6 -20
  6. package/dist/dkg/verification.js +257 -10
  7. package/dist/elgamal/additive.d.ts +0 -10
  8. package/dist/elgamal/additive.js +3 -11
  9. package/dist/elgamal/types.d.ts +0 -7
  10. package/dist/index.d.ts +11 -2
  11. package/dist/index.js +10 -1
  12. package/dist/proofs/disjunctive.js +1 -1
  13. package/dist/proofs/dleq.js +1 -1
  14. package/dist/proofs/nonces.js +1 -1
  15. package/dist/proofs/schnorr.js +1 -1
  16. package/dist/protocol/builders.d.ts +33 -10
  17. package/dist/protocol/builders.js +12 -5
  18. package/dist/protocol/canonical-json.js +1 -1
  19. package/dist/protocol/manifest.d.ts +6 -1
  20. package/dist/protocol/manifest.js +16 -2
  21. package/dist/protocol/ordering.js +1 -1
  22. package/dist/protocol/payloads.d.ts +16 -0
  23. package/dist/protocol/payloads.js +24 -1
  24. package/dist/protocol/types.d.ts +1 -0
  25. package/dist/protocol/verification.js +25 -10
  26. package/dist/protocol/voting-ballot-aggregation.js +1 -1
  27. package/dist/protocol/voting-decryption.js +10 -1
  28. package/dist/protocol/voting-shared.js +4 -3
  29. package/dist/protocol/voting-verification.js +82 -5
  30. package/dist/serialize/encoding.d.ts +0 -16
  31. package/dist/serialize/encoding.js +2 -18
  32. package/dist/threshold/decrypt.d.ts +11 -1
  33. package/dist/threshold/decrypt.js +45 -2
  34. package/dist/threshold/types.d.ts +15 -0
  35. package/dist/transport/auth.d.ts +2 -1
  36. package/dist/transport/auth.js +1 -2
  37. package/dist/transport/complaints.d.ts +7 -1
  38. package/dist/transport/complaints.js +1 -2
  39. package/dist/transport/envelopes.js +2 -2
  40. package/dist/transport/key-agreement.d.ts +2 -8
  41. package/dist/transport/key-agreement.js +2 -3
  42. package/dist/transport/types.d.ts +0 -6
  43. package/package.json +2 -8
  44. package/dist/dkg/checkpoints.d.ts +0 -19
  45. package/dist/dkg/checkpoints.js +0 -87
  46. package/dist/dkg/verification-complaints.d.ts +0 -10
  47. package/dist/dkg/verification-complaints.js +0 -166
  48. package/dist/elgamal/index.d.ts +0 -3
  49. package/dist/elgamal/index.js +0 -3
  50. package/dist/proofs/index.d.ts +0 -6
  51. package/dist/proofs/index.js +0 -6
  52. package/dist/protocol/index.d.ts +0 -16
  53. package/dist/protocol/index.js +0 -16
  54. package/dist/protocol/voting-ballot-close.d.ts +0 -23
  55. package/dist/protocol/voting-ballot-close.js +0 -63
  56. package/dist/serialize/index.d.ts +0 -1
  57. package/dist/serialize/index.js +0 -1
  58. package/dist/threshold/index.d.ts +0 -3
  59. package/dist/threshold/index.js +0 -3
  60. package/dist/transport/index.d.ts +0 -6
  61. package/dist/transport/index.js +0 -6
  62. package/dist/vss/index.d.ts +0 -4
  63. package/dist/vss/index.js +0 -4
package/README.md CHANGED
@@ -193,12 +193,13 @@ The root package also exposes builders for the signed protocol payloads used acr
193
193
  - decryption shares
194
194
  - tally publication
195
195
 
196
- For the reveal path, the public root surface is intentionally two-step:
196
+ For the reveal path, the public root surface is intentionally three-step:
197
197
 
198
+ - prepare the accepted aggregate with `prepareAggregateForDecryption(...)`
198
199
  - compute each partial share with `createDecryptionShare(...)`
199
200
  - prove and publish it with `createDLEQProof(...)` and `createDecryptionSharePayload(...)`
200
201
 
201
- After collecting a threshold subset, recover the tally with `combineDecryptionShares(...)`.
202
+ After collecting a threshold subset, recover the tally with `combineDecryptionShares(...)` against the prepared aggregate ciphertext.
202
203
 
203
204
  For concrete posted JSON shapes, use [Published payload examples](https://tenemo.github.io/threshold-elgamal/guides/published-payload-examples/).
204
205
 
@@ -1,4 +1,4 @@
1
- export * from './bigint.js';
1
+ export { modInvQ, modQ } from './bigint.js';
2
2
  export * from './crypto.js';
3
3
  export * from './errors.js';
4
4
  export * from './groups.js';
@@ -1,4 +1,4 @@
1
- export * from './bigint.js';
1
+ export { modInvQ, modQ } from './bigint.js';
2
2
  export * from './crypto.js';
3
3
  export * from './errors.js';
4
4
  export * from './groups.js';
@@ -1,6 +1,6 @@
1
1
  import { InvalidPayloadError, RISTRETTO_GROUP } from '../core/index.js';
2
2
  import { canonicalizeJson } from '../protocol/canonical-json.js';
3
- import { bigintToFixedHex, fixedHexToBigInt } from '../serialize/index.js';
3
+ import { bigintToFixedHex, fixedHexToBigInt } from '../serialize/encoding.js';
4
4
  const pedersenShareEnvelopeKeys = [
5
5
  'blindingValue',
6
6
  'index',
@@ -1,7 +1,6 @@
1
1
  import { type CryptoGroup } from '../core/index.js';
2
2
  import type { EncodedPoint } from '../core/types.js';
3
- import type { ComplaintPayload, ElectionManifest, EncryptedDualSharePayload, RegistrationPayload, SignedPayload } from '../protocol/types.js';
4
- import { type FinalizedPhaseCheckpoint } from './checkpoints.js';
3
+ import type { ComplaintPayload, ElectionManifest, PhaseCheckpointPayload, RegistrationPayload, SignedPayload } from '../protocol/types.js';
5
4
  /** Input bundle for verifying a DKG transcript. */
6
5
  export type VerifyDKGTranscriptInput = {
7
6
  readonly transcript: readonly SignedPayload[];
@@ -26,25 +25,12 @@ export type VerifiedDKGTranscript = {
26
25
  readonly rosterHash: string;
27
26
  readonly threshold: number;
28
27
  };
29
- export type EncryptedShareMatrix = {
30
- readonly encryptedShares: readonly SignedPayload<EncryptedDualSharePayload>[];
31
- readonly bySlot: ReadonlyMap<string, SignedPayload<EncryptedDualSharePayload>>;
32
- readonly byComplaintKey: ReadonlyMap<string, SignedPayload<EncryptedDualSharePayload>>;
28
+ /** Finalized threshold-supported checkpoint for one DKG phase. */
29
+ export type FinalizedPhaseCheckpoint = {
30
+ readonly payload: PhaseCheckpointPayload;
31
+ readonly signatures: readonly SignedPayload<PhaseCheckpointPayload>[];
32
+ readonly signers: readonly number[];
33
33
  };
34
- export type ResolvePhaseCheckpointInput = {
35
- readonly transcript: readonly SignedPayload[];
36
- readonly checkpointPhase: number;
37
- readonly threshold: number;
38
- readonly participantCount: number;
39
- readonly expectedQualifiedParticipantIndices: readonly number[];
40
- readonly signerUniverse: ReadonlySet<number>;
41
- };
42
- export declare const parseCommitmentVector: (commitments: readonly string[], expectedLength: number, label: string) => readonly EncodedPoint[];
43
- export declare const complaintResolutionKey: (complainantIndex: number, dealerIndex: number, envelopeId: string) => string;
44
- export declare const encryptedShareSlotKey: (dealerIndex: number, recipientIndex: number) => string;
45
- export declare const validateParticipantIndex: (index: number, participantCount: number, label: string) => void;
46
- export declare const assertUniqueSortedParticipantIndices: (indices: readonly number[], participantCount: number, label: string) => void;
47
- export declare const assertIndexSubset: (indices: readonly number[], allowed: ReadonlySet<number>, label: string) => void;
48
34
  /**
49
35
  * Derives the transcript verification key `Y_j` for one participant index from
50
36
  * published Feldman commitments.
@@ -1,12 +1,13 @@
1
1
  import { InvalidPayloadError, RISTRETTO_GROUP, ThresholdViolationError, assertCanonicalRistrettoGroup, assertInSubgroupOrIdentity, assertPositiveParticipantIndex, majorityThreshold, modQ, } from '../core/index.js';
2
2
  import { RISTRETTO_ZERO, decodePoint, decodeScalar, encodePoint, pointAdd, pointMultiply, } from '../core/ristretto.js';
3
- import { verifySchnorrProof } from '../proofs/index.js';
3
+ import { verifySchnorrProof } from '../proofs/schnorr.js';
4
4
  import { auditSignedPayloads } from '../protocol/board-audit.js';
5
5
  import { hashElectionManifest, SHIPPED_PROTOCOL_VERSION, } from '../protocol/manifest.js';
6
- import { hashProtocolTranscript } from '../protocol/transcript.js';
6
+ import { hashProtocolPhaseSnapshot, hashProtocolTranscript, } from '../protocol/transcript.js';
7
7
  import { verifySignedProtocolPayloads, } from '../protocol/verification.js';
8
- import { assertSupportedCheckpointPayloads, isPhaseCheckpointPayload, resolveVerifiedPhaseCheckpoint, } from './checkpoints.js';
9
- import { assertEncryptedShareCoverage, assertPedersenCommitmentCoverage, buildEncryptedShareMatrix, parsePedersenCommitmentMap, verifyComplaintOutcomes, } from './verification-complaints.js';
8
+ import { resolveDealerChallengeFromPublicKey } from '../transport/complaints.js';
9
+ import { verifyPedersenShare } from '../vss/pedersen.js';
10
+ import { decodePedersenShareEnvelope } from './pedersen-share-codec.js';
10
11
  const GJKR_PHASE_PLAN = {
11
12
  'manifest-publication': 0,
12
13
  registration: 0,
@@ -41,7 +42,7 @@ const requireExactlyOnePayload = (payloads, label) => {
41
42
  }
42
43
  return payloads[0];
43
44
  };
44
- export const parseCommitmentVector = (commitments, expectedLength, label) => {
45
+ const parseCommitmentVector = (commitments, expectedLength, label) => {
45
46
  if (commitments.length !== expectedLength) {
46
47
  throw new InvalidPayloadError(`${label} must contain exactly ${expectedLength} commitments`);
47
48
  }
@@ -63,15 +64,15 @@ const buildSchnorrContext = (payload, protocolVersion, coefficientIndex) => ({
63
64
  participantIndex: payload.participantIndex,
64
65
  coefficientIndex,
65
66
  });
66
- export const complaintResolutionKey = (complainantIndex, dealerIndex, envelopeId) => `${complainantIndex}:${dealerIndex}:${envelopeId}`;
67
- export const encryptedShareSlotKey = (dealerIndex, recipientIndex) => `${dealerIndex}:${recipientIndex}`;
67
+ const complaintResolutionKey = (complainantIndex, dealerIndex, envelopeId) => `${complainantIndex}:${dealerIndex}:${envelopeId}`;
68
+ const encryptedShareSlotKey = (dealerIndex, recipientIndex) => `${dealerIndex}:${recipientIndex}`;
68
69
  const allParticipantIndices = (participantCount) => Array.from({ length: participantCount }, (_value, index) => index + 1);
69
- export const validateParticipantIndex = (index, participantCount, label) => {
70
+ const validateParticipantIndex = (index, participantCount, label) => {
70
71
  if (!Number.isInteger(index) || index < 1 || index > participantCount) {
71
72
  throw new InvalidPayloadError(`${label} ${index} must satisfy 1 <= j <= n (n = ${participantCount})`);
72
73
  }
73
74
  };
74
- export const assertUniqueSortedParticipantIndices = (indices, participantCount, label) => {
75
+ const assertUniqueSortedParticipantIndices = (indices, participantCount, label) => {
75
76
  let previous = 0;
76
77
  const seen = new Set();
77
78
  for (const index of indices) {
@@ -86,6 +87,10 @@ export const assertUniqueSortedParticipantIndices = (indices, participantCount,
86
87
  previous = index;
87
88
  }
88
89
  };
90
+ /** Returns `true` when the signed payload is a phase checkpoint. */
91
+ function isPhaseCheckpointPayload(payload) {
92
+ return payload.payload.messageType === 'phase-checkpoint';
93
+ }
89
94
  const validateTranscriptShape = (input, manifestHash) => {
90
95
  for (const signedPayload of input.transcript) {
91
96
  const expected = expectedDKGPhase(signedPayload.payload.messageType, isPhaseCheckpointPayload(signedPayload)
@@ -102,13 +107,255 @@ const validateTranscriptShape = (input, manifestHash) => {
102
107
  }
103
108
  }
104
109
  };
105
- export const assertIndexSubset = (indices, allowed, label) => {
110
+ const assertIndexSubset = (indices, allowed, label) => {
106
111
  for (const index of indices) {
107
112
  if (!allowed.has(index)) {
108
113
  throw new InvalidPayloadError(`${label} ${index} is not part of the allowed participant set`);
109
114
  }
110
115
  }
111
116
  };
117
+ const checkpointKey = (payload) => JSON.stringify({
118
+ sessionId: payload.sessionId,
119
+ manifestHash: payload.manifestHash,
120
+ phase: payload.phase,
121
+ messageType: payload.messageType,
122
+ checkpointPhase: payload.checkpointPhase,
123
+ checkpointTranscriptHash: payload.checkpointTranscriptHash,
124
+ qualifiedParticipantIndices: payload.qualifiedParticipantIndices,
125
+ });
126
+ const compareNumbers = (left, right) => left - right;
127
+ const sameParticipantIndexList = (left, right) => left.length === right.length &&
128
+ left.every((participantIndex, index) => participantIndex === right[index]);
129
+ const requiredCheckpointPhases = () => [0, 1, 2, 3];
130
+ const collectCheckpointVariants = (transcript, checkpointPhase) => {
131
+ const grouped = new Map();
132
+ for (const signedPayload of transcript) {
133
+ if (!isPhaseCheckpointPayload(signedPayload) ||
134
+ signedPayload.payload.checkpointPhase !== checkpointPhase) {
135
+ continue;
136
+ }
137
+ const key = checkpointKey(signedPayload.payload);
138
+ const existing = grouped.get(key) ??
139
+ new Map();
140
+ existing.set(signedPayload.payload.participantIndex, signedPayload);
141
+ grouped.set(key, existing);
142
+ }
143
+ return [...grouped.values()].map((signatureMap) => {
144
+ const signatures = [...signatureMap.values()];
145
+ return {
146
+ payload: signatures[0].payload,
147
+ signatures,
148
+ signers: signatures
149
+ .map((entry) => entry.payload.participantIndex)
150
+ .sort(compareNumbers),
151
+ };
152
+ });
153
+ };
154
+ /**
155
+ * Rejects phase-checkpoint payloads outside the shipped GJKR checkpoint plan.
156
+ */
157
+ const assertSupportedCheckpointPayloads = (transcript) => {
158
+ for (const signedPayload of transcript) {
159
+ if (isPhaseCheckpointPayload(signedPayload) &&
160
+ !requiredCheckpointPhases().includes(signedPayload.payload.checkpointPhase)) {
161
+ throw new InvalidPayloadError(`Checkpoint phase ${signedPayload.payload.checkpointPhase} is not part of the GJKR phase plan`);
162
+ }
163
+ }
164
+ };
165
+ /**
166
+ * Resolves and validates the unique threshold-supported checkpoint for one
167
+ * closed DKG phase.
168
+ */
169
+ const resolveVerifiedPhaseCheckpoint = async (input) => {
170
+ const supported = collectCheckpointVariants(input.transcript, input.checkpointPhase).filter((entry) => entry.signatures.length >= input.threshold);
171
+ if (supported.length === 0) {
172
+ throw new InvalidPayloadError(`Missing threshold-supported phase checkpoint for phase ${input.checkpointPhase}`);
173
+ }
174
+ if (supported.length > 1) {
175
+ throw new InvalidPayloadError(`Observed multiple threshold-supported phase checkpoints for phase ${input.checkpointPhase}`);
176
+ }
177
+ const checkpoint = supported[0];
178
+ const qualifiedParticipantIndices = checkpoint.payload.qualifiedParticipantIndices;
179
+ assertUniqueSortedParticipantIndices(qualifiedParticipantIndices, input.participantCount, `Phase ${input.checkpointPhase} checkpoint qualified participant`);
180
+ if (!sameParticipantIndexList(qualifiedParticipantIndices, input.expectedQualifiedParticipantIndices)) {
181
+ throw new InvalidPayloadError(`Phase ${input.checkpointPhase} checkpoint qualified participant set does not match the verifier-computed active participant set`);
182
+ }
183
+ if (qualifiedParticipantIndices.length < input.threshold) {
184
+ throw new InvalidPayloadError(`Checkpoint qualified participant set for phase ${input.checkpointPhase} must contain at least ${input.threshold} participants`);
185
+ }
186
+ const expectedSnapshotHash = await hashProtocolPhaseSnapshot(input.transcript.map((entry) => entry.payload), input.checkpointPhase);
187
+ if (checkpoint.payload.checkpointTranscriptHash !== expectedSnapshotHash) {
188
+ throw new InvalidPayloadError(`Phase ${input.checkpointPhase} checkpoint transcript hash does not match the signed transcript snapshot`);
189
+ }
190
+ assertIndexSubset(checkpoint.signers, input.signerUniverse, `Phase ${input.checkpointPhase} checkpoint signer`);
191
+ const qualifiedParticipantSet = new Set(qualifiedParticipantIndices);
192
+ for (const signer of checkpoint.signers) {
193
+ if (!qualifiedParticipantSet.has(signer)) {
194
+ throw new InvalidPayloadError(`Phase ${input.checkpointPhase} checkpoint signer ${signer} is not part of the checkpoint qualified participant set`);
195
+ }
196
+ }
197
+ return checkpoint;
198
+ };
199
+ const buildEncryptedShareMatrix = (transcript, participantCount) => {
200
+ const encryptedShares = transcript.filter((payload) => payload.payload.messageType === 'encrypted-dual-share');
201
+ const bySlot = new Map();
202
+ const byComplaintKey = new Map();
203
+ for (const payload of encryptedShares) {
204
+ const dealerIndex = payload.payload.participantIndex;
205
+ const recipientIndex = payload.payload.recipientIndex;
206
+ validateParticipantIndex(dealerIndex, participantCount, 'Encrypted share dealer index');
207
+ validateParticipantIndex(recipientIndex, participantCount, 'Encrypted share recipient index');
208
+ if (dealerIndex === recipientIndex) {
209
+ throw new InvalidPayloadError(`Encrypted share payload for dealer ${dealerIndex} must target a different recipient`);
210
+ }
211
+ const slotKey = encryptedShareSlotKey(dealerIndex, recipientIndex);
212
+ if (bySlot.has(slotKey)) {
213
+ throw new InvalidPayloadError(`Duplicate encrypted share payload for dealer ${dealerIndex} and recipient ${recipientIndex}`);
214
+ }
215
+ bySlot.set(slotKey, payload);
216
+ const complaintKey = complaintResolutionKey(recipientIndex, dealerIndex, payload.payload.envelopeId);
217
+ if (byComplaintKey.has(complaintKey)) {
218
+ throw new InvalidPayloadError(`Duplicate encrypted share envelope ${payload.payload.envelopeId} for dealer ${dealerIndex} and recipient ${recipientIndex}`);
219
+ }
220
+ byComplaintKey.set(complaintKey, payload);
221
+ }
222
+ return {
223
+ encryptedShares,
224
+ bySlot,
225
+ byComplaintKey,
226
+ };
227
+ };
228
+ const assertEncryptedShareCoverage = (encryptedShareMatrix, participantIndices) => {
229
+ for (const dealerIndex of participantIndices) {
230
+ for (const recipientIndex of participantIndices) {
231
+ if (dealerIndex === recipientIndex) {
232
+ continue;
233
+ }
234
+ if (!encryptedShareMatrix.bySlot.has(encryptedShareSlotKey(dealerIndex, recipientIndex))) {
235
+ throw new InvalidPayloadError(`Missing encrypted share payload for dealer ${dealerIndex} and recipient ${recipientIndex}`);
236
+ }
237
+ }
238
+ }
239
+ };
240
+ const parsePedersenCommitmentMap = (transcript, threshold) => {
241
+ const pedersenCommitments = transcript.filter((payload) => payload.payload.messageType === 'pedersen-commitment');
242
+ const pedersenCommitmentMap = new Map();
243
+ for (const payload of pedersenCommitments) {
244
+ if (pedersenCommitmentMap.has(payload.payload.participantIndex)) {
245
+ throw new InvalidPayloadError(`Pedersen commitment requires exactly one payload for participant ${payload.payload.participantIndex}`);
246
+ }
247
+ pedersenCommitmentMap.set(payload.payload.participantIndex, parseCommitmentVector(payload.payload.commitments, threshold, 'Pedersen commitment payload'));
248
+ }
249
+ return pedersenCommitmentMap;
250
+ };
251
+ const assertPedersenCommitmentCoverage = (pedersenCommitmentMap, dealerIndices) => {
252
+ for (const dealerIndex of dealerIndices) {
253
+ if (!pedersenCommitmentMap.has(dealerIndex)) {
254
+ throw new InvalidPayloadError(`Missing Pedersen commitment payload for dealer ${dealerIndex}`);
255
+ }
256
+ }
257
+ };
258
+ const buildComplaintResolutionPayloadMap = (transcript) => {
259
+ const complaintResolutionPayloads = transcript
260
+ .filter((payload) => payload.payload.messageType === 'complaint-resolution')
261
+ .map((payload) => payload.payload);
262
+ const resolutionPayloadMap = new Map();
263
+ for (const resolutionPayload of complaintResolutionPayloads) {
264
+ if (resolutionPayload.participantIndex !== resolutionPayload.dealerIndex) {
265
+ throw new InvalidPayloadError(`Complaint resolution for envelope ${resolutionPayload.envelopeId} must be authored by dealer ${resolutionPayload.dealerIndex}`);
266
+ }
267
+ const key = complaintResolutionKey(resolutionPayload.complainantIndex, resolutionPayload.dealerIndex, resolutionPayload.envelopeId);
268
+ if (resolutionPayloadMap.has(key)) {
269
+ throw new InvalidPayloadError(`Duplicate complaint resolution for envelope ${resolutionPayload.envelopeId}`);
270
+ }
271
+ resolutionPayloadMap.set(key, resolutionPayload);
272
+ }
273
+ return {
274
+ complaintResolutionPayloads,
275
+ resolutionPayloadMap,
276
+ };
277
+ };
278
+ const verifyComplaintOutcomes = async (input, verifiedSignatures, encryptedShareMatrix, pedersenCommitmentMap, group, allowedParticipants) => {
279
+ const complaints = input.transcript
280
+ .filter((payload) => payload.payload.messageType === 'complaint')
281
+ .map((payload) => payload.payload);
282
+ const { complaintResolutionPayloads, resolutionPayloadMap } = buildComplaintResolutionPayloadMap(input.transcript);
283
+ const rosterEntryMap = new Map(verifiedSignatures.rosterEntries.map((entry) => [
284
+ entry.participantIndex,
285
+ entry,
286
+ ]));
287
+ const acceptedComplaints = [];
288
+ const usedResolutionKeys = new Set();
289
+ for (const complaint of complaints) {
290
+ if (!allowedParticipants.has(complaint.participantIndex) ||
291
+ !allowedParticipants.has(complaint.dealerIndex)) {
292
+ throw new InvalidPayloadError(`Complaint participants must belong to the active DKG set for phase ${complaint.phase}`);
293
+ }
294
+ const resolutionKey = complaintResolutionKey(complaint.participantIndex, complaint.dealerIndex, complaint.envelopeId);
295
+ const matchingEnvelope = encryptedShareMatrix.byComplaintKey.get(resolutionKey);
296
+ if (matchingEnvelope === undefined) {
297
+ throw new InvalidPayloadError(`Complaint references an unknown envelope ${complaint.envelopeId} for dealer ${complaint.dealerIndex} and complainant ${complaint.participantIndex}`);
298
+ }
299
+ const resolutionPayload = resolutionPayloadMap.get(resolutionKey);
300
+ if (resolutionPayload === undefined) {
301
+ acceptedComplaints.push(complaint);
302
+ continue;
303
+ }
304
+ usedResolutionKeys.add(resolutionKey);
305
+ if (resolutionPayload.suite !== matchingEnvelope.payload.suite) {
306
+ acceptedComplaints.push(complaint);
307
+ continue;
308
+ }
309
+ const complainantRosterEntry = rosterEntryMap.get(complaint.participantIndex);
310
+ if (complainantRosterEntry === undefined) {
311
+ throw new InvalidPayloadError(`Missing roster entry for complainant ${complaint.participantIndex}`);
312
+ }
313
+ const dealerCommitments = pedersenCommitmentMap.get(complaint.dealerIndex);
314
+ if (dealerCommitments === undefined) {
315
+ throw new InvalidPayloadError(`Missing Pedersen commitments for dealer ${complaint.dealerIndex}`);
316
+ }
317
+ try {
318
+ const resolution = await resolveDealerChallengeFromPublicKey({
319
+ ...matchingEnvelope.payload,
320
+ dealerIndex: matchingEnvelope.payload.participantIndex,
321
+ rosterHash: input.manifest.rosterHash,
322
+ payloadType: 'encrypted-dual-share',
323
+ protocolVersion: SHIPPED_PROTOCOL_VERSION,
324
+ }, complainantRosterEntry.transportPublicKey, resolutionPayload.revealedEphemeralPrivateKey);
325
+ if (resolution.valid !== true ||
326
+ resolution.plaintext === undefined) {
327
+ acceptedComplaints.push(complaint);
328
+ continue;
329
+ }
330
+ const decryptedShare = decodePedersenShareEnvelope(resolution.plaintext, complaint.participantIndex, 'Complaint resolution');
331
+ if (!verifyPedersenShare(decryptedShare, {
332
+ commitments: dealerCommitments,
333
+ }, group)) {
334
+ acceptedComplaints.push(complaint);
335
+ continue;
336
+ }
337
+ }
338
+ catch (error) {
339
+ if (error instanceof InvalidPayloadError) {
340
+ acceptedComplaints.push(complaint);
341
+ continue;
342
+ }
343
+ throw error;
344
+ }
345
+ }
346
+ for (const resolutionPayload of complaintResolutionPayloads) {
347
+ if (!allowedParticipants.has(resolutionPayload.participantIndex) ||
348
+ !allowedParticipants.has(resolutionPayload.dealerIndex) ||
349
+ !allowedParticipants.has(resolutionPayload.complainantIndex)) {
350
+ throw new InvalidPayloadError(`Complaint resolution participants must belong to the active DKG set for phase ${resolutionPayload.phase}`);
351
+ }
352
+ const key = complaintResolutionKey(resolutionPayload.complainantIndex, resolutionPayload.dealerIndex, resolutionPayload.envelopeId);
353
+ if (!usedResolutionKeys.has(key)) {
354
+ throw new InvalidPayloadError(`Complaint resolution for envelope ${resolutionPayload.envelopeId} does not match any complaint`);
355
+ }
356
+ }
357
+ return acceptedComplaints;
358
+ };
112
359
  const deriveTranscriptVerificationKeyInternal = (commitmentSets, participantIndex, group) => {
113
360
  assertCanonicalRistrettoGroup(group, 'Transcript verification-key derivation group');
114
361
  assertPositiveParticipantIndex(participantIndex);
@@ -1,14 +1,4 @@
1
1
  import type { ElGamalCiphertext } from './types.js';
2
- /**
3
- * Validates that a private key lies in the range `1..q-1`.
4
- */
5
- export declare const assertValidPrivateKey: (privateKey: bigint) => void;
6
- /** Validates an additive-mode public key against the shipped suite. */
7
- export declare const assertValidAdditivePublicKey: (publicKey: string) => void;
8
- /** Validates the caller-supplied additive plaintext bound. */
9
- export declare const assertValidAdditiveBound: (bound: bigint) => void;
10
- /** Validates the plaintext domain and caller-supplied bound for additive mode. */
11
- export declare const assertValidAdditivePlaintext: (value: bigint, bound: bigint) => void;
12
2
  /** Validates an additive ciphertext that may already be an aggregate. */
13
3
  export declare const assertValidAdditiveCiphertext: (ciphertext: ElGamalCiphertext) => void;
14
4
  /**
@@ -1,21 +1,13 @@
1
1
  import { RISTRETTO_GROUP, assertAdditiveBound, assertInSubgroupOrIdentity, assertPlaintextAdditive, assertValidPublicKey, InvalidScalarError, } from '../core/index.js';
2
2
  import { decodePoint, encodePoint, multiplyBase, pointAdd, pointMultiply, } from '../core/ristretto.js';
3
- /**
4
- * Validates that a private key lies in the range `1..q-1`.
5
- */
6
- export const assertValidPrivateKey = (privateKey) => {
7
- if (privateKey <= 0n || privateKey >= RISTRETTO_GROUP.q) {
8
- throw new InvalidScalarError('Private key must be in the range 1..q-1');
9
- }
10
- };
11
3
  /** Validates an additive-mode public key against the shipped suite. */
12
- export const assertValidAdditivePublicKey = (publicKey) => {
4
+ const assertValidAdditivePublicKey = (publicKey) => {
13
5
  assertValidPublicKey(publicKey);
14
6
  };
15
7
  /** Validates the caller-supplied additive plaintext bound. */
16
- export const assertValidAdditiveBound = (bound) => assertAdditiveBound(bound, RISTRETTO_GROUP.q);
8
+ const assertValidAdditiveBound = (bound) => assertAdditiveBound(bound, RISTRETTO_GROUP.q);
17
9
  /** Validates the plaintext domain and caller-supplied bound for additive mode. */
18
- export const assertValidAdditivePlaintext = (value, bound) => assertPlaintextAdditive(value, bound, RISTRETTO_GROUP.q);
10
+ const assertValidAdditivePlaintext = (value, bound) => assertPlaintextAdditive(value, bound, RISTRETTO_GROUP.q);
19
11
  /** Validates an additive ciphertext that may already be an aggregate. */
20
12
  export const assertValidAdditiveCiphertext = (ciphertext) => {
21
13
  assertInSubgroupOrIdentity(ciphertext.c1);
@@ -1,11 +1,4 @@
1
1
  import type { EncodedPoint } from '../core/types.js';
2
- /** Public and private key pair for the shipped Ristretto255 suite. */
3
- export type ElGamalKeyPair = {
4
- /** Public key `Y = xG` encoded as a canonical Ristretto point. */
5
- readonly publicKey: EncodedPoint;
6
- /** Private scalar `x` in the range `1..q-1`. */
7
- readonly privateKey: bigint;
8
- };
9
2
  /** Standard additive ElGamal ciphertext pair `(c1, c2)` encoded as points. */
10
3
  export type ElGamalCiphertext = {
11
4
  /** Ephemeral component `rG`. */
package/dist/index.d.ts CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Safe root package exports for the current surface.
3
+ *
4
+ * Use this entry point for group definitions, additive ElGamal, validation
5
+ * helpers, and serialization helpers that are safe for the shipped package.
6
+ *
7
+ * @module threshold-elgamal
8
+ * @packageDocumentation
9
+ */
1
10
  export { IndexOutOfRangeError, InvalidGroupElementError, InvalidPayloadError, InvalidProofError, InvalidScalarError, InvalidShareError, PhaseViolationError, PlaintextDomainError, ThresholdViolationError, TranscriptMismatchError, UnsupportedSuiteError, } from './core/errors.js';
2
11
  export { modQ } from './core/bigint.js';
3
12
  export { RISTRETTO_GROUP } from './core/groups.js';
@@ -12,8 +21,8 @@ export { decryptEnvelope, encryptEnvelope } from './transport/envelopes.js';
12
21
  export { exportAuthPublicKey, generateAuthKeyPair } from './transport/auth.js';
13
22
  export { exportTransportPublicKey, generateTransportKeyPair, } from './transport/key-agreement.js';
14
23
  export type { EncodedAuthPublicKey, EncodedTransportPublicKey, EncryptedEnvelope, EnvelopeContext, TransportKeyPair, } from './transport/types.js';
15
- export { combineDecryptionShares, createDecryptionShare, } from './threshold/decrypt.js';
16
- export type { DecryptionShare, Share, VerifiedAggregateCiphertext, } from './threshold/types.js';
24
+ export { combineDecryptionShares, createDecryptionShare, prepareAggregateForDecryption, } from './threshold/decrypt.js';
25
+ export type { AggregateDecryptionPreparationInput, DecryptionShare, Share, VerifiedAggregateCiphertext, } from './threshold/types.js';
17
26
  export { deriveJointPublicKey, deriveTranscriptVerificationKey, verifyDKGTranscript, } from './dkg/verification.js';
18
27
  export { decodePedersenShareEnvelope, encodePedersenShareEnvelope, } from './dkg/pedersen-share-codec.js';
19
28
  export type { VerifyDKGTranscriptInput, VerifiedDKGTranscript, } from './dkg/verification.js';
package/dist/index.js CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Safe root package exports for the current surface.
3
+ *
4
+ * Use this entry point for group definitions, additive ElGamal, validation
5
+ * helpers, and serialization helpers that are safe for the shipped package.
6
+ *
7
+ * @module threshold-elgamal
8
+ * @packageDocumentation
9
+ */
1
10
  export { IndexOutOfRangeError, InvalidGroupElementError, InvalidPayloadError, InvalidProofError, InvalidScalarError, InvalidShareError, PhaseViolationError, PlaintextDomainError, ThresholdViolationError, TranscriptMismatchError, UnsupportedSuiteError, } from './core/errors.js';
2
11
  export { modQ } from './core/bigint.js';
3
12
  export { RISTRETTO_GROUP } from './core/groups.js';
@@ -9,7 +18,7 @@ export { createSchnorrProof, verifySchnorrProof } from './proofs/schnorr.js';
9
18
  export { decryptEnvelope, encryptEnvelope } from './transport/envelopes.js';
10
19
  export { exportAuthPublicKey, generateAuthKeyPair } from './transport/auth.js';
11
20
  export { exportTransportPublicKey, generateTransportKeyPair, } from './transport/key-agreement.js';
12
- export { combineDecryptionShares, createDecryptionShare, } from './threshold/decrypt.js';
21
+ export { combineDecryptionShares, createDecryptionShare, prepareAggregateForDecryption, } from './threshold/decrypt.js';
13
22
  export { deriveJointPublicKey, deriveTranscriptVerificationKey, verifyDKGTranscript, } from './dkg/verification.js';
14
23
  export { decodePedersenShareEnvelope, encodePedersenShareEnvelope, } from './dkg/pedersen-share-codec.js';
15
24
  export { generateFeldmanCommitments, verifyFeldmanShare } from './vss/feldman.js';
@@ -1,6 +1,6 @@
1
1
  import { assertInSubgroup, assertInSubgroupOrIdentity, assertScalarInZq, InvalidProofError, modQ, randomScalarBelow, } from '../core/index.js';
2
2
  import { decodePoint, encodePoint, multiplyBase, pointMultiply, pointSubtract, } from '../core/ristretto.js';
3
- import { concatBytes, encodeForChallenge, encodeSequenceForChallenge, } from '../serialize/index.js';
3
+ import { concatBytes, encodeForChallenge, encodeSequenceForChallenge, } from '../serialize/encoding.js';
4
4
  import { assertProofContext, contextElements, fixedPoint, fixedScalar, hashChallenge, sumChallenges, } from './helpers.js';
5
5
  import { hedgedNonce } from './nonces.js';
6
6
  const candidateEncoding = (ciphertext, candidateValue, group) => encodePoint(pointSubtract(decodePoint(ciphertext.c2, 'Ciphertext c2'), multiplyBase(modQ(candidateValue, group.q))));
@@ -1,6 +1,6 @@
1
1
  import { assertInSubgroup, assertInSubgroupOrIdentity, assertScalarInZq, modQ, } from '../core/index.js';
2
2
  import { decodePoint, encodePoint, multiplyBase, pointMultiply, pointSubtract, } from '../core/ristretto.js';
3
- import { encodeForChallenge } from '../serialize/index.js';
3
+ import { encodeForChallenge } from '../serialize/encoding.js';
4
4
  import { assertProofContext, contextElements, fixedPoint, hashChallenge, } from './helpers.js';
5
5
  import { hedgedNonce } from './nonces.js';
6
6
  const nonceContext = (statement, group, context) => encodeForChallenge(...contextElements(context), fixedPoint(group.g), fixedPoint(statement.ciphertext.c1), fixedPoint(statement.ciphertext.c2), fixedPoint(statement.publicKey), fixedPoint(statement.decryptionShare));
@@ -1,6 +1,6 @@
1
1
  import { bytesToBigInt } from '../core/bytes.js';
2
2
  import { modQ, randomBytes, sha256, } from '../core/index.js';
3
- import { bigintToFixedBytes, concatBytes, domainSeparator, } from '../serialize/index.js';
3
+ import { bigintToFixedBytes, concatBytes, domainSeparator, } from '../serialize/encoding.js';
4
4
  const bitLength = (value) => value === 0n ? 0 : value.toString(2).length;
5
5
  const encodeCounter = (value) => {
6
6
  const bytes = new Uint8Array(4);
@@ -1,6 +1,6 @@
1
1
  import { assertInSubgroup, assertScalarInZq, modQ, } from '../core/index.js';
2
2
  import { decodePoint, encodePoint, multiplyBase, pointMultiply, pointSubtract, } from '../core/ristretto.js';
3
- import { encodeForChallenge } from '../serialize/index.js';
3
+ import { encodeForChallenge } from '../serialize/encoding.js';
4
4
  import { assertProofContext, contextElements, fixedPoint, hashChallenge, } from './helpers.js';
5
5
  import { hedgedNonce } from './nonces.js';
6
6
  const nonceContext = (statement, group, context) => encodeForChallenge(...contextElements(context), fixedPoint(group.g), fixedPoint(statement));
@@ -1,14 +1,21 @@
1
1
  import type { ElGamalCiphertext } from '../elgamal/types.js';
2
2
  import type { DLEQProof, DisjunctiveProof } from '../proofs/types.js';
3
3
  import type { EncodedAuthPublicKey, EncodedTransportPublicKey } from '../transport/types.js';
4
- import type { BallotClosePayload, BallotSubmissionPayload, DecryptionSharePayload, ElectionManifest, EncodedCompactProof, EncryptedDualSharePayload, FeldmanCommitmentPayload, KeyDerivationConfirmation, ManifestAcceptancePayload, ManifestPublicationPayload, PedersenCommitmentPayload, PhaseCheckpointPayload, ProtocolPayload, RegistrationPayload, SignedPayload, TallyPublicationPayload } from './types.js';
4
+ import type { ProtocolPayloadInput } from './payloads.js';
5
+ import type { BallotClosePayload, BallotSubmissionPayload, DecryptionSharePayload, ElectionManifest, EncodedCompactProof, EncryptedDualSharePayload, FeldmanCommitmentPayload, KeyDerivationConfirmation, ManifestAcceptancePayload, ManifestPublicationPayload, PedersenCommitmentPayload, PhaseCheckpointPayload, ProtocolMessageType, ProtocolPayload, RegistrationPayload, SignedPayload, TallyPublicationPayload } from './types.js';
6
+ type ProtocolPayloadByMessageType<TMessageType extends ProtocolMessageType> = Extract<ProtocolPayload, {
7
+ readonly messageType: TMessageType;
8
+ }>;
5
9
  /** Signs one canonical protocol payload with a participant auth key. */
6
- export declare const signProtocolPayload: <TPayload extends ProtocolPayload>(privateKey: CryptoKey, payload: TPayload) => Promise<SignedPayload<TPayload>>;
10
+ export declare const signProtocolPayload: <TMessageType extends ProtocolMessageType>(privateKey: CryptoKey, payload: ProtocolPayloadInput<ProtocolPayloadByMessageType<TMessageType>> & {
11
+ readonly messageType: TMessageType;
12
+ }) => Promise<SignedPayload<ProtocolPayloadByMessageType<TMessageType>>>;
7
13
  /** Creates a signed manifest-publication payload. */
8
14
  export declare const createManifestPublicationPayload: (privateKey: CryptoKey, input: {
9
15
  readonly manifest: ElectionManifest;
10
16
  readonly manifestHash: string;
11
17
  readonly participantIndex: number;
18
+ readonly protocolVersion?: string;
12
19
  readonly sessionId: string;
13
20
  }) => Promise<SignedPayload<ManifestPublicationPayload>>;
14
21
  /** Creates a signed registration payload for one participant. */
@@ -16,6 +23,7 @@ export declare const createRegistrationPayload: (privateKey: CryptoKey, input: {
16
23
  readonly authPublicKey: EncodedAuthPublicKey;
17
24
  readonly manifestHash: string;
18
25
  readonly participantIndex: number;
26
+ readonly protocolVersion?: string;
19
27
  readonly rosterHash: string;
20
28
  readonly sessionId: string;
21
29
  readonly transportPublicKey: EncodedTransportPublicKey;
@@ -26,6 +34,7 @@ export declare const createManifestAcceptancePayload: (privateKey: CryptoKey, in
26
34
  readonly assignedParticipantIndex: number;
27
35
  readonly manifestHash: string;
28
36
  readonly participantIndex: number;
37
+ readonly protocolVersion?: string;
29
38
  readonly rosterHash: string;
30
39
  readonly sessionId: string;
31
40
  }) => Promise<SignedPayload<ManifestAcceptancePayload>>;
@@ -34,16 +43,22 @@ export declare const createPhaseCheckpointPayload: (privateKey: CryptoKey, input
34
43
  readonly checkpointPhase: 0 | 1 | 2 | 3;
35
44
  readonly manifestHash: string;
36
45
  readonly participantIndex: number;
46
+ readonly protocolVersion?: string;
37
47
  readonly qualifiedParticipantIndices: readonly number[];
38
48
  readonly sessionId: string;
39
49
  readonly transcript: readonly SignedPayload[];
40
50
  }) => Promise<SignedPayload<PhaseCheckpointPayload>>;
41
51
  /** Creates a signed Pedersen-commitment payload for DKG phase 1. */
42
- export declare const createPedersenCommitmentPayload: (privateKey: CryptoKey, input: Omit<PedersenCommitmentPayload, "messageType" | "phase">) => Promise<SignedPayload<PedersenCommitmentPayload>>;
52
+ export declare const createPedersenCommitmentPayload: (privateKey: CryptoKey, input: Omit<PedersenCommitmentPayload, "messageType" | "phase" | "protocolVersion"> & {
53
+ readonly protocolVersion?: string;
54
+ }) => Promise<SignedPayload<PedersenCommitmentPayload>>;
43
55
  /** Creates a signed encrypted dual-share payload for DKG phase 1. */
44
- export declare const createEncryptedDualSharePayload: (privateKey: CryptoKey, input: Omit<EncryptedDualSharePayload, "messageType" | "phase">) => Promise<SignedPayload<EncryptedDualSharePayload>>;
56
+ export declare const createEncryptedDualSharePayload: (privateKey: CryptoKey, input: Omit<EncryptedDualSharePayload, "messageType" | "phase" | "protocolVersion"> & {
57
+ readonly protocolVersion?: string;
58
+ }) => Promise<SignedPayload<EncryptedDualSharePayload>>;
45
59
  /** Creates a signed Feldman-commitment payload for DKG phase 3. */
46
- export declare const createFeldmanCommitmentPayload: (privateKey: CryptoKey, input: Omit<FeldmanCommitmentPayload, "messageType" | "phase" | "proofs"> & {
60
+ export declare const createFeldmanCommitmentPayload: (privateKey: CryptoKey, input: Omit<FeldmanCommitmentPayload, "messageType" | "phase" | "proofs" | "protocolVersion"> & {
61
+ readonly protocolVersion?: string;
47
62
  readonly proofs: FeldmanCommitmentPayload["proofs"] | readonly ({
48
63
  readonly coefficientIndex: number;
49
64
  readonly challenge: bigint;
@@ -53,19 +68,27 @@ export declare const createFeldmanCommitmentPayload: (privateKey: CryptoKey, inp
53
68
  } & EncodedCompactProof))[];
54
69
  }) => Promise<SignedPayload<FeldmanCommitmentPayload>>;
55
70
  /** Creates a signed key-derivation-confirmation payload for DKG phase 4. */
56
- export declare const createKeyDerivationConfirmationPayload: (privateKey: CryptoKey, input: Omit<KeyDerivationConfirmation, "messageType" | "phase">) => Promise<SignedPayload<KeyDerivationConfirmation>>;
71
+ export declare const createKeyDerivationConfirmationPayload: (privateKey: CryptoKey, input: Omit<KeyDerivationConfirmation, "messageType" | "phase" | "protocolVersion"> & {
72
+ readonly protocolVersion?: string;
73
+ }) => Promise<SignedPayload<KeyDerivationConfirmation>>;
57
74
  /** Creates a signed ballot payload for one participant and one option slot. */
58
- export declare const createBallotSubmissionPayload: (privateKey: CryptoKey, input: Omit<BallotSubmissionPayload, "messageType" | "phase" | "ciphertext" | "proof"> & {
75
+ export declare const createBallotSubmissionPayload: (privateKey: CryptoKey, input: Omit<BallotSubmissionPayload, "messageType" | "phase" | "ciphertext" | "proof" | "protocolVersion"> & {
76
+ readonly protocolVersion?: string;
59
77
  readonly ciphertext: BallotSubmissionPayload["ciphertext"] | ElGamalCiphertext;
60
78
  readonly proof: BallotSubmissionPayload["proof"] | DisjunctiveProof;
61
79
  }) => Promise<SignedPayload<BallotSubmissionPayload>>;
62
80
  /** Creates the organizer-signed ballot cutoff payload. */
63
- export declare const createBallotClosePayload: (privateKey: CryptoKey, input: Omit<BallotClosePayload, "messageType" | "phase">) => Promise<SignedPayload<BallotClosePayload>>;
81
+ export declare const createBallotClosePayload: (privateKey: CryptoKey, input: Omit<BallotClosePayload, "messageType" | "phase" | "protocolVersion"> & {
82
+ readonly protocolVersion?: string;
83
+ }) => Promise<SignedPayload<BallotClosePayload>>;
64
84
  /** Creates a signed threshold decryption-share payload for one option slot. */
65
- export declare const createDecryptionSharePayload: (privateKey: CryptoKey, input: Omit<DecryptionSharePayload, "messageType" | "phase" | "proof"> & {
85
+ export declare const createDecryptionSharePayload: (privateKey: CryptoKey, input: Omit<DecryptionSharePayload, "messageType" | "phase" | "proof" | "protocolVersion"> & {
86
+ readonly protocolVersion?: string;
66
87
  readonly proof: DecryptionSharePayload["proof"] | DLEQProof;
67
88
  }) => Promise<SignedPayload<DecryptionSharePayload>>;
68
89
  /** Creates a signed tally-publication payload for one option slot. */
69
- export declare const createTallyPublicationPayload: (privateKey: CryptoKey, input: Omit<TallyPublicationPayload, "messageType" | "phase" | "tally"> & {
90
+ export declare const createTallyPublicationPayload: (privateKey: CryptoKey, input: Omit<TallyPublicationPayload, "messageType" | "phase" | "tally" | "protocolVersion"> & {
91
+ readonly protocolVersion?: string;
70
92
  readonly tally: TallyPublicationPayload["tally"] | bigint;
71
93
  }) => Promise<SignedPayload<TallyPublicationPayload>>;
94
+ export {};