threshold-elgamal 1.1.0 → 2.1.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.
- package/README.md +25 -15
- package/dist/core/bigint.d.ts +2 -1
- package/dist/core/bigint.js +2 -1
- package/dist/dkg/verification.d.ts +9 -2
- package/dist/dkg/verification.js +9 -2
- package/dist/elgamal/additive.js +6 -20
- package/dist/index.d.ts +27 -11
- package/dist/index.js +20 -10
- package/dist/proofs/disjunctive.d.ts +4 -3
- package/dist/proofs/disjunctive.js +5 -4
- package/dist/proofs/helpers.d.ts +1 -1
- package/dist/proofs/helpers.js +1 -4
- package/dist/proofs/types.d.ts +2 -2
- package/dist/protocol/builders.d.ts +1 -1
- package/dist/protocol/builders.js +1 -1
- package/dist/protocol/manifest.d.ts +3 -3
- package/dist/protocol/manifest.js +29 -5
- package/dist/protocol/public.d.ts +7 -5
- package/dist/protocol/public.js +6 -4
- package/dist/protocol/score-range.d.ts +6 -0
- package/dist/protocol/score-range.js +24 -0
- package/dist/protocol/types.d.ts +18 -5
- package/dist/protocol/voting-ballots.d.ts +4 -3
- package/dist/protocol/voting-ballots.js +4 -3
- package/dist/protocol/voting-codecs.d.ts +7 -4
- package/dist/protocol/voting-codecs.js +13 -3
- package/dist/protocol/voting-decryption.d.ts +2 -1
- package/dist/protocol/voting-decryption.js +2 -1
- package/dist/protocol/voting-shared.d.ts +2 -1
- package/dist/protocol/voting-shared.js +3 -2
- package/dist/protocol/voting-verification.d.ts +2 -2
- package/dist/protocol/voting-verification.js +6 -7
- package/package.json +13 -12
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# threshold-elgamal
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/threshold-elgamal)
|
|
3
|
+
[](https://www.npmjs.com/package/threshold-elgamal) [](https://www.npmjs.com/package/threshold-elgamal)
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
- additive ElGamal on `ristretto255`
|
|
18
18
|
- honest-majority GJKR DKG
|
|
19
|
-
-
|
|
20
|
-
-
|
|
19
|
+
- one explicit global contiguous score range per ceremony
|
|
20
|
+
- manifest `scoreRange.max` capped at `100` to keep proofs and tally recovery tractable
|
|
21
|
+
- one public manifest shape: `rosterHash`, `optionList`, and `scoreRange`
|
|
21
22
|
- organizer-signed `ballot-close` before decryption
|
|
22
23
|
- full local recomputation and full ceremony verification from the public board
|
|
23
24
|
|
|
@@ -77,10 +78,10 @@ Older browsers, stale embedded webviews, and runtimes without Web Crypto `X25519
|
|
|
77
78
|
The supported boardroom flow is:
|
|
78
79
|
|
|
79
80
|
1. Freeze the roster in the application and hash it with `hashRosterEntries(...)`.
|
|
80
|
-
2. Build the
|
|
81
|
+
2. Build the manifest with `createElectionManifest({ rosterHash, optionList, scoreRange })`.
|
|
81
82
|
3. Publish the manifest, registrations, and manifest acceptances.
|
|
82
83
|
4. Run the honest-majority GJKR transcript.
|
|
83
|
-
5. Post ballot payloads for complete
|
|
84
|
+
5. Post ballot payloads for complete scores inside the manifest-declared range.
|
|
84
85
|
6. Post one organizer-signed `ballot-close` payload that freezes which complete ballots are counted.
|
|
85
86
|
7. Post threshold decryption shares and tally publications for the close-selected ballot set.
|
|
86
87
|
8. Verify the whole ceremony with `verifyElectionCeremony(...)`.
|
|
@@ -93,7 +94,7 @@ The cryptographic threshold is derived internally from the accepted registration
|
|
|
93
94
|
|
|
94
95
|
There is no supported `n-of-n` mode and no supported public `k-of-n` configuration.
|
|
95
96
|
|
|
96
|
-
Transcript verification requires key-derivation
|
|
97
|
+
Transcript verification requires `key-derivation-confirmation` payloads from every qualified participant. In the current design those unanimous confirmations are part of verifier soundness: the library does not implement a public post-Feldman complaint/reconstruction phase, so the DKG verifier is participant-confirmed rather than fully public-data-only. Lowering confirmation acceptance to threshold-many is out of scope unless that missing public consistency machinery is added.
|
|
97
98
|
|
|
98
99
|
See [Honest-majority voting flow](https://tenemo.github.io/threshold-elgamal/guides/three-participant-voting-flow/) for the full phase-by-phase transcript.
|
|
99
100
|
|
|
@@ -129,6 +130,7 @@ const rosterHash = await hashRosterEntries([
|
|
|
129
130
|
const manifest = createElectionManifest({
|
|
130
131
|
rosterHash,
|
|
131
132
|
optionList: ["Option A", "Option B"],
|
|
133
|
+
scoreRange: { min: 0, max: 5 },
|
|
132
134
|
});
|
|
133
135
|
|
|
134
136
|
const manifestHash = await hashElectionManifest(manifest);
|
|
@@ -143,6 +145,10 @@ console.log(majorityThreshold(3)); // 2
|
|
|
143
145
|
console.log(sessionId.length); // 64
|
|
144
146
|
```
|
|
145
147
|
|
|
148
|
+
The example uses `0..5` only as one concrete score range. The supported rule is
|
|
149
|
+
one manifest-declared contiguous range with non-negative bounds and
|
|
150
|
+
`scoreRange.max <= 100`.
|
|
151
|
+
|
|
146
152
|
If your application consumes a complete public board, start with [Verifying a public board](https://tenemo.github.io/threshold-elgamal/guides/verifying-a-public-board/) and then move directly to the verifier entry point:
|
|
147
153
|
|
|
148
154
|
```typescript
|
|
@@ -156,7 +162,7 @@ const bundle: VerifyElectionCeremonyInput = {
|
|
|
156
162
|
sessionId,
|
|
157
163
|
dkgTranscript,
|
|
158
164
|
ballotPayloads,
|
|
159
|
-
ballotClosePayload,
|
|
165
|
+
ballotClosePayloads: [ballotClosePayload],
|
|
160
166
|
decryptionSharePayloads,
|
|
161
167
|
tallyPublications,
|
|
162
168
|
};
|
|
@@ -171,7 +177,9 @@ if (!result.ok) {
|
|
|
171
177
|
}
|
|
172
178
|
```
|
|
173
179
|
|
|
174
|
-
|
|
180
|
+
Pass the full published `ballot-close` slot in `ballotClosePayloads`, even when the normal case is one organizer payload. The verifier audits that slot, collapses only exact retransmissions, and requires exactly one accepted close record.
|
|
181
|
+
|
|
182
|
+
The root package exposes the builders and lower-level helpers required for the documented ceremony, including:
|
|
175
183
|
|
|
176
184
|
- manifest publication
|
|
177
185
|
- registration
|
|
@@ -186,14 +194,16 @@ The root package exposes the workflow-facing builders for the signed protocol pa
|
|
|
186
194
|
- decryption shares
|
|
187
195
|
- tally publication
|
|
188
196
|
|
|
189
|
-
|
|
197
|
+
The reveal path also works from the root package:
|
|
198
|
+
|
|
199
|
+
- prepare the accepted aggregate with `prepareAggregateForDecryption(...)`
|
|
200
|
+
- compute each partial share with `createDecryptionShare(...)`
|
|
201
|
+
- prove it with `createDLEQProof(...)`
|
|
202
|
+
- publish it with `createDecryptionSharePayload(...)`
|
|
190
203
|
|
|
191
|
-
|
|
192
|
-
- compute each partial share with `createDecryptionShare(...)` from `threshold-elgamal/threshold`
|
|
193
|
-
- prove it with `createDLEQProof(...)` from `threshold-elgamal/proofs`
|
|
194
|
-
- publish it with `createDecryptionSharePayload(...)` from `threshold-elgamal`
|
|
204
|
+
After collecting a threshold subset, recover the tally with `combineDecryptionShares(...)` against the prepared aggregate ciphertext.
|
|
195
205
|
|
|
196
|
-
|
|
206
|
+
The grouped public submodules remain available when you prefer narrower imports by subsystem, but the supported full ceremony does not require them.
|
|
197
207
|
|
|
198
208
|
For concrete posted JSON shapes, use [Published payload examples](https://tenemo.github.io/threshold-elgamal/guides/published-payload-examples/).
|
|
199
209
|
|
|
@@ -204,7 +214,7 @@ The library is designed for an honest-origin, honest-client, static-adversary se
|
|
|
204
214
|
What it tries to enforce:
|
|
205
215
|
|
|
206
216
|
- additive-only tallying on `ristretto255`
|
|
207
|
-
-
|
|
217
|
+
- one explicit global contiguous manifest score range
|
|
208
218
|
- grouped per-option ballot verification
|
|
209
219
|
- mandatory local aggregate recomputation before decryption
|
|
210
220
|
- organizer-visible and auditable ballot cutoff through `ballot-close`
|
package/dist/core/bigint.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ export declare const mod: (value: bigint, modulus: bigint) => bigint;
|
|
|
7
7
|
/**
|
|
8
8
|
* Reduces a value into the range `0..q-1`.
|
|
9
9
|
*
|
|
10
|
-
* @throws {@link InvalidScalarError
|
|
10
|
+
* @throws {@link threshold-elgamal/core!InvalidScalarError | InvalidScalarError}
|
|
11
|
+
* When `q` is not positive.
|
|
11
12
|
*/
|
|
12
13
|
export declare const modQ: (value: bigint, q: bigint) => bigint;
|
|
13
14
|
/**
|
package/dist/core/bigint.js
CHANGED
|
@@ -70,7 +70,8 @@ const modP = (value, p) => mod(value, p);
|
|
|
70
70
|
/**
|
|
71
71
|
* Reduces a value into the range `0..q-1`.
|
|
72
72
|
*
|
|
73
|
-
* @throws {@link InvalidScalarError
|
|
73
|
+
* @throws {@link threshold-elgamal/core!InvalidScalarError | InvalidScalarError}
|
|
74
|
+
* When `q` is not positive.
|
|
74
75
|
*/
|
|
75
76
|
export const modQ = (value, q) => mod(value, q);
|
|
76
77
|
/**
|
|
@@ -78,9 +78,16 @@ export declare const deriveJointPublicKey: (feldmanCommitments: readonly {
|
|
|
78
78
|
/**
|
|
79
79
|
* Verifies a DKG transcript, its signatures, Feldman extraction proofs, the
|
|
80
80
|
* exact claimed threshold degree, accepted complaint outcomes, the DKG
|
|
81
|
-
* transcript hash,
|
|
81
|
+
* transcript hash, the announced joint public key, and unanimous qualified
|
|
82
|
+
* participant key confirmations.
|
|
82
83
|
*
|
|
83
84
|
* This is the DKG-specific verifier that the full ceremony verifier delegates
|
|
84
|
-
* to before it touches ballots or tally material.
|
|
85
|
+
* to before it touches ballots or tally material. It consumes a public signed
|
|
86
|
+
* transcript plus `key-derivation-confirmation` payloads from every qualified
|
|
87
|
+
* participant. It does not implement a public post-Feldman
|
|
88
|
+
* complaint/reconstruction phase, so it is a participant-confirmed transcript
|
|
89
|
+
* verifier rather than a fully public-data-only GJKR verifier. Lowering this
|
|
90
|
+
* requirement to threshold-many confirmations is out of scope unless that
|
|
91
|
+
* missing public consistency machinery is added.
|
|
85
92
|
*/
|
|
86
93
|
export declare const verifyDKGTranscript: (input: VerifyDKGTranscriptInput) => Promise<VerifiedDKGTranscript>;
|
package/dist/dkg/verification.js
CHANGED
|
@@ -659,10 +659,17 @@ const verifyCheckpointedDKGTranscript = async (input, setup) => {
|
|
|
659
659
|
/**
|
|
660
660
|
* Verifies a DKG transcript, its signatures, Feldman extraction proofs, the
|
|
661
661
|
* exact claimed threshold degree, accepted complaint outcomes, the DKG
|
|
662
|
-
* transcript hash,
|
|
662
|
+
* transcript hash, the announced joint public key, and unanimous qualified
|
|
663
|
+
* participant key confirmations.
|
|
663
664
|
*
|
|
664
665
|
* This is the DKG-specific verifier that the full ceremony verifier delegates
|
|
665
|
-
* to before it touches ballots or tally material.
|
|
666
|
+
* to before it touches ballots or tally material. It consumes a public signed
|
|
667
|
+
* transcript plus `key-derivation-confirmation` payloads from every qualified
|
|
668
|
+
* participant. It does not implement a public post-Feldman
|
|
669
|
+
* complaint/reconstruction phase, so it is a participant-confirmed transcript
|
|
670
|
+
* verifier rather than a fully public-data-only GJKR verifier. Lowering this
|
|
671
|
+
* requirement to threshold-many confirmations is out of scope unless that
|
|
672
|
+
* missing public consistency machinery is added.
|
|
666
673
|
*/
|
|
667
674
|
export const verifyDKGTranscript = async (input) => {
|
|
668
675
|
const auditedTranscript = await auditSignedPayloads(input.transcript);
|
package/dist/elgamal/additive.js
CHANGED
|
@@ -6,14 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { RISTRETTO_GROUP, assertAdditiveBound, assertInSubgroupOrIdentity, assertPlaintextAdditive, assertValidPublicKey, InvalidScalarError, } from '../core/index.js';
|
|
8
8
|
import { decodePoint, encodePoint, multiplyBase, pointAdd, pointMultiply, } from '../core/ristretto.js';
|
|
9
|
-
/** Validates an additive-mode public key against the built-in suite. */
|
|
10
|
-
const assertValidAdditivePublicKey = (publicKey) => {
|
|
11
|
-
assertValidPublicKey(publicKey);
|
|
12
|
-
};
|
|
13
|
-
/** Validates the caller-supplied additive plaintext bound. */
|
|
14
|
-
const assertValidAdditiveBound = (bound) => assertAdditiveBound(bound, RISTRETTO_GROUP.q);
|
|
15
|
-
/** Validates the plaintext domain and caller-supplied bound for additive mode. */
|
|
16
|
-
const assertValidAdditivePlaintext = (value, bound) => assertPlaintextAdditive(value, bound, RISTRETTO_GROUP.q);
|
|
17
9
|
/**
|
|
18
10
|
* Validates an additive ciphertext that may already represent an aggregate.
|
|
19
11
|
*
|
|
@@ -24,19 +16,12 @@ export const assertValidAdditiveCiphertext = (ciphertext) => {
|
|
|
24
16
|
assertInSubgroupOrIdentity(ciphertext.c1);
|
|
25
17
|
assertInSubgroupOrIdentity(ciphertext.c2);
|
|
26
18
|
};
|
|
27
|
-
const
|
|
19
|
+
const requireAdditiveBound = (bound) => {
|
|
28
20
|
if (typeof bound !== 'bigint') {
|
|
29
|
-
throw new InvalidScalarError(
|
|
21
|
+
throw new InvalidScalarError('Additive encryption requires an explicit plaintext bound');
|
|
30
22
|
}
|
|
31
23
|
return bound;
|
|
32
24
|
};
|
|
33
|
-
const resolveAdditiveContext = (bound, operation) => {
|
|
34
|
-
const resolvedBound = resolveAdditiveBound(bound, operation);
|
|
35
|
-
assertValidAdditiveBound(resolvedBound);
|
|
36
|
-
return {
|
|
37
|
-
bound: resolvedBound,
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
25
|
const assertEncryptionRandomness = (randomness) => {
|
|
41
26
|
if (randomness <= 0n || randomness >= RISTRETTO_GROUP.q) {
|
|
42
27
|
throw new InvalidScalarError('Encryption randomness must be in the range 1..q-1');
|
|
@@ -75,9 +60,10 @@ export const addEncryptedValues = (left, right) => {
|
|
|
75
60
|
* the randomness input and plaintext bound.
|
|
76
61
|
*/
|
|
77
62
|
export const encryptAdditiveWithRandomness = (message, publicKey, randomness, bound) => {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
63
|
+
const resolvedBound = requireAdditiveBound(bound);
|
|
64
|
+
assertAdditiveBound(resolvedBound, RISTRETTO_GROUP.q);
|
|
65
|
+
assertPlaintextAdditive(message, resolvedBound, RISTRETTO_GROUP.q);
|
|
66
|
+
assertValidPublicKey(publicKey);
|
|
81
67
|
assertEncryptionRandomness(randomness);
|
|
82
68
|
return encryptAdditiveWithValidatedInputs(message, publicKey, randomness);
|
|
83
69
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Root package exports for the supported public API.
|
|
3
3
|
*
|
|
4
|
-
* Use this entry point for the supported voting workflow: manifest and
|
|
5
|
-
* setup, transport keys and envelopes,
|
|
6
|
-
*
|
|
4
|
+
* Use this entry point for the full supported voting workflow: manifest and
|
|
5
|
+
* roster setup, transport keys and envelopes, DKG commitments and share
|
|
6
|
+
* handling, ballot encryption and proofs, decryption-share publication, tally
|
|
7
|
+
* reconstruction, and full ceremony verification.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Public subpath modules such as `threshold-elgamal/proofs`,
|
|
10
|
+
* `threshold-elgamal/threshold`, and `threshold-elgamal/dkg` remain available
|
|
11
|
+
* when you prefer grouped imports by subsystem, but the supported ceremony can
|
|
12
|
+
* be implemented from this root package alone.
|
|
12
13
|
*
|
|
13
14
|
* @module threshold-elgamal
|
|
14
15
|
* @packageDocumentation
|
|
15
16
|
*/
|
|
17
|
+
export { RISTRETTO_GROUP, modQ } from './core/public.js';
|
|
18
|
+
export type { EncodedPoint } from './core/public.js';
|
|
16
19
|
export { majorityThreshold } from './core/validation.js';
|
|
20
|
+
export { encryptAdditiveWithRandomness } from './elgamal/public.js';
|
|
21
|
+
export type { ElGamalCiphertext } from './elgamal/public.js';
|
|
22
|
+
export { deriveJointPublicKey, deriveTranscriptVerificationKey, encodePedersenShareEnvelope, decodePedersenShareEnvelope, verifyDKGTranscript, } from './dkg/public.js';
|
|
23
|
+
export type { VerifyDKGTranscriptInput, VerifiedDKGTranscript, } from './dkg/public.js';
|
|
24
|
+
export { createDisjunctiveProof, createDLEQProof, createSchnorrProof, verifyDisjunctiveProof, verifyDLEQProof, verifySchnorrProof, } from './proofs/public.js';
|
|
25
|
+
export type { DLEQProof, DisjunctiveProof, DLEQStatement, ProofContext, SchnorrProof, } from './proofs/public.js';
|
|
17
26
|
export { decryptEnvelope, encryptEnvelope } from './transport/envelopes.js';
|
|
18
27
|
export { exportAuthPublicKey, generateAuthKeyPair } from './transport/auth.js';
|
|
19
28
|
export { exportTransportPublicKey, generateTransportKeyPair, } from './transport/key-agreement.js';
|
|
20
29
|
export type { EncodedAuthPublicKey, EncodedTransportPublicKey, EncryptedEnvelope, EnvelopeContext, TransportKeyPair, } from './transport/types.js';
|
|
21
|
-
export { createBallotClosePayload, createBallotSubmissionPayload, createDecryptionSharePayload, createEncryptedDualSharePayload, createFeldmanCommitmentPayload, createKeyDerivationConfirmationPayload, createManifestAcceptancePayload, createManifestPublicationPayload, createPedersenCommitmentPayload, createPhaseCheckpointPayload, createRegistrationPayload, createTallyPublicationPayload, } from './protocol/builders.js';
|
|
30
|
+
export { createBallotClosePayload, createBallotSubmissionPayload, createDecryptionSharePayload, createEncryptedDualSharePayload, createFeldmanCommitmentPayload, createKeyDerivationConfirmationPayload, createManifestAcceptancePayload, createManifestPublicationPayload, createPedersenCommitmentPayload, createPhaseCheckpointPayload, createRegistrationPayload, signProtocolPayload, createTallyPublicationPayload, } from './protocol/builders.js';
|
|
22
31
|
export { canonicalizeElectionManifest, createElectionManifest, deriveSessionId, hashElectionManifest, SHIPPED_PROTOCOL_VERSION, validateElectionManifest, } from './protocol/manifest.js';
|
|
23
|
-
export { hashRosterEntries } from './protocol/verification.js';
|
|
32
|
+
export { hashRosterEntries, verifySignedProtocolPayloads, type RosterEntry, type VerifiedProtocolSignatures, } from './protocol/verification.js';
|
|
33
|
+
export { hashProtocolTranscript } from './protocol/transcript.js';
|
|
34
|
+
export { verifyBallotSubmissionPayloadsByOption } from './protocol/voting-ballots.js';
|
|
35
|
+
export { scoreRangeDomain } from './protocol/voting-codecs.js';
|
|
24
36
|
export { verifyElectionCeremony, tryVerifyElectionCeremony, type ElectionVerificationErrorCode, type ElectionVerificationFailure, type ElectionVerificationResult, type ElectionVerificationStage, type VerifiedElectionCeremony, } from './protocol/voting-verification.js';
|
|
25
|
-
export type { BallotClosePayload, BallotSubmissionPayload, DecryptionSharePayload, ElectionManifest, EncryptedDualSharePayload, FeldmanCommitmentPayload, KeyDerivationConfirmation, ManifestAcceptancePayload, ManifestPublicationPayload, PedersenCommitmentPayload, PhaseCheckpointPayload, RegistrationPayload, SignedPayload, TallyPublicationPayload, VerifyElectionCeremonyInput, } from './protocol/types.js';
|
|
37
|
+
export type { BallotClosePayload, BallotSubmissionPayload, DecryptionSharePayload, ElectionManifest, EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof, EncryptedDualSharePayload, FeldmanCommitmentPayload, KeyDerivationConfirmation, ManifestAcceptancePayload, ManifestPublicationPayload, PedersenCommitmentPayload, PhaseCheckpointPayload, ProtocolMessageType, ProtocolPayload, RegistrationPayload, ScoreRange, SignedPayload, TallyPublicationPayload, VerifyElectionCeremonyInput, } from './protocol/types.js';
|
|
38
|
+
export { combineDecryptionShares, createDecryptionShare, prepareAggregateForDecryption, } from './threshold/public.js';
|
|
39
|
+
export type { AggregateDecryptionPreparationInput, DecryptionShare, Share, VerifiedAggregateCiphertext, } from './threshold/public.js';
|
|
40
|
+
export { derivePedersenShares, generateFeldmanCommitments, generatePedersenCommitments, verifyFeldmanShare, verifyPedersenShare, } from './vss/public.js';
|
|
41
|
+
export type { FeldmanCommitments, PedersenCommitments, PedersenShare, } from './vss/public.js';
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Root package exports for the supported public API.
|
|
3
3
|
*
|
|
4
|
-
* Use this entry point for the supported voting workflow: manifest and
|
|
5
|
-
* setup, transport keys and envelopes,
|
|
6
|
-
*
|
|
4
|
+
* Use this entry point for the full supported voting workflow: manifest and
|
|
5
|
+
* roster setup, transport keys and envelopes, DKG commitments and share
|
|
6
|
+
* handling, ballot encryption and proofs, decryption-share publication, tally
|
|
7
|
+
* reconstruction, and full ceremony verification.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Public subpath modules such as `threshold-elgamal/proofs`,
|
|
10
|
+
* `threshold-elgamal/threshold`, and `threshold-elgamal/dkg` remain available
|
|
11
|
+
* when you prefer grouped imports by subsystem, but the supported ceremony can
|
|
12
|
+
* be implemented from this root package alone.
|
|
12
13
|
*
|
|
13
14
|
* @module threshold-elgamal
|
|
14
15
|
* @packageDocumentation
|
|
15
16
|
*/
|
|
17
|
+
export { RISTRETTO_GROUP, modQ } from './core/public.js';
|
|
16
18
|
export { majorityThreshold } from './core/validation.js';
|
|
19
|
+
export { encryptAdditiveWithRandomness } from './elgamal/public.js';
|
|
20
|
+
export { deriveJointPublicKey, deriveTranscriptVerificationKey, encodePedersenShareEnvelope, decodePedersenShareEnvelope, verifyDKGTranscript, } from './dkg/public.js';
|
|
21
|
+
export { createDisjunctiveProof, createDLEQProof, createSchnorrProof, verifyDisjunctiveProof, verifyDLEQProof, verifySchnorrProof, } from './proofs/public.js';
|
|
17
22
|
export { decryptEnvelope, encryptEnvelope } from './transport/envelopes.js';
|
|
18
23
|
export { exportAuthPublicKey, generateAuthKeyPair } from './transport/auth.js';
|
|
19
24
|
export { exportTransportPublicKey, generateTransportKeyPair, } from './transport/key-agreement.js';
|
|
20
|
-
export { createBallotClosePayload, createBallotSubmissionPayload, createDecryptionSharePayload, createEncryptedDualSharePayload, createFeldmanCommitmentPayload, createKeyDerivationConfirmationPayload, createManifestAcceptancePayload, createManifestPublicationPayload, createPedersenCommitmentPayload, createPhaseCheckpointPayload, createRegistrationPayload, createTallyPublicationPayload, } from './protocol/builders.js';
|
|
25
|
+
export { createBallotClosePayload, createBallotSubmissionPayload, createDecryptionSharePayload, createEncryptedDualSharePayload, createFeldmanCommitmentPayload, createKeyDerivationConfirmationPayload, createManifestAcceptancePayload, createManifestPublicationPayload, createPedersenCommitmentPayload, createPhaseCheckpointPayload, createRegistrationPayload, signProtocolPayload, createTallyPublicationPayload, } from './protocol/builders.js';
|
|
21
26
|
export { canonicalizeElectionManifest, createElectionManifest, deriveSessionId, hashElectionManifest, SHIPPED_PROTOCOL_VERSION, validateElectionManifest, } from './protocol/manifest.js';
|
|
22
|
-
export { hashRosterEntries } from './protocol/verification.js';
|
|
27
|
+
export { hashRosterEntries, verifySignedProtocolPayloads, } from './protocol/verification.js';
|
|
28
|
+
export { hashProtocolTranscript } from './protocol/transcript.js';
|
|
29
|
+
export { verifyBallotSubmissionPayloadsByOption } from './protocol/voting-ballots.js';
|
|
30
|
+
export { scoreRangeDomain } from './protocol/voting-codecs.js';
|
|
23
31
|
export { verifyElectionCeremony, tryVerifyElectionCeremony, } from './protocol/voting-verification.js';
|
|
32
|
+
export { combineDecryptionShares, createDecryptionShare, prepareAggregateForDecryption, } from './threshold/public.js';
|
|
33
|
+
export { derivePedersenShares, generateFeldmanCommitments, generatePedersenCommitments, verifyFeldmanShare, verifyPedersenShare, } from './vss/public.js';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CDS94-style disjunctive proofs for additive
|
|
2
|
+
* CDS94-style disjunctive proofs for additive ElGamal plaintexts drawn from an
|
|
3
|
+
* explicit finite value set.
|
|
3
4
|
*
|
|
4
5
|
* Ballot payloads use this module to prove that a ciphertext encodes one value
|
|
5
|
-
* from the
|
|
6
|
+
* from the manifest-declared domain without revealing which value was chosen.
|
|
6
7
|
*/
|
|
7
8
|
import { type CryptoGroup, type RandomBytesSource } from '../core/index.js';
|
|
8
9
|
import type { ElGamalCiphertext } from '../elgamal/types.js';
|
|
@@ -18,6 +19,6 @@ export declare const createDisjunctiveProof: (plaintext: bigint, randomness: big
|
|
|
18
19
|
* Verifies a CDS94-style disjunctive proof for additive ElGamal plaintexts.
|
|
19
20
|
*
|
|
20
21
|
* Ballot verification uses this to reject ciphertexts that do not encode one
|
|
21
|
-
* of the allowed
|
|
22
|
+
* of the allowed manifest-domain values for the current option slot.
|
|
22
23
|
*/
|
|
23
24
|
export declare const verifyDisjunctiveProof: (proof: DisjunctiveProof, ciphertext: ElGamalCiphertext, publicKey: string, validValues: readonly bigint[], group: CryptoGroup, context: ProofContext) => Promise<boolean>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CDS94-style disjunctive proofs for additive
|
|
2
|
+
* CDS94-style disjunctive proofs for additive ElGamal plaintexts drawn from an
|
|
3
|
+
* explicit finite value set.
|
|
3
4
|
*
|
|
4
5
|
* Ballot payloads use this module to prove that a ciphertext encodes one value
|
|
5
|
-
* from the
|
|
6
|
+
* from the manifest-declared domain without revealing which value was chosen.
|
|
6
7
|
*/
|
|
7
8
|
import { assertInSubgroup, assertInSubgroupOrIdentity, assertScalarInZq, InvalidProofError, modQ, randomScalarBelow, } from '../core/index.js';
|
|
8
9
|
import { decodePoint, encodePoint, multiplyBase, pointMultiply, pointSubtract, } from '../core/ristretto.js';
|
|
@@ -11,7 +12,7 @@ import { assertProofContext, contextElements, fixedPoint, fixedScalar, hashChall
|
|
|
11
12
|
import { hedgedNonce } from './nonces.js';
|
|
12
13
|
const candidateEncoding = (ciphertext, candidateValue, group) => encodePoint(pointSubtract(decodePoint(ciphertext.c2, 'Ciphertext c2'), multiplyBase(modQ(candidateValue, group.q))));
|
|
13
14
|
const commitmentSequence = (commitments) => encodeSequenceForChallenge(commitments.map((commitment) => concatBytes(encodeForChallenge(fixedPoint(commitment.a1)), encodeForChallenge(fixedPoint(commitment.a2)))));
|
|
14
|
-
const challengePayload = (ciphertext, publicKey, validValues, commitments, group, context) => encodeForChallenge(...contextElements(context), fixedPoint(group.g), fixedPoint(publicKey), fixedPoint(ciphertext.c1), fixedPoint(ciphertext.c2), encodeSequenceForChallenge(validValues.map((value) => fixedScalar(modQ(value, group.q)
|
|
15
|
+
const challengePayload = (ciphertext, publicKey, validValues, commitments, group, context) => encodeForChallenge(...contextElements(context), fixedPoint(group.g), fixedPoint(publicKey), fixedPoint(ciphertext.c1), fixedPoint(ciphertext.c2), encodeSequenceForChallenge(validValues.map((value) => fixedScalar(modQ(value, group.q)))), commitmentSequence(commitments));
|
|
15
16
|
/**
|
|
16
17
|
* Creates a CDS94-style disjunctive proof for additive ElGamal plaintexts.
|
|
17
18
|
*
|
|
@@ -67,7 +68,7 @@ export const createDisjunctiveProof = async (plaintext, randomness, ciphertext,
|
|
|
67
68
|
* Verifies a CDS94-style disjunctive proof for additive ElGamal plaintexts.
|
|
68
69
|
*
|
|
69
70
|
* Ballot verification uses this to reject ciphertexts that do not encode one
|
|
70
|
-
* of the allowed
|
|
71
|
+
* of the allowed manifest-domain values for the current option slot.
|
|
71
72
|
*/
|
|
72
73
|
export const verifyDisjunctiveProof = async (proof, ciphertext, publicKey, validValues, group, context) => {
|
|
73
74
|
assertProofContext(context, group);
|
package/dist/proofs/helpers.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ import type { ProofContext } from './types.js';
|
|
|
3
3
|
export declare const assertProofContext: (context: ProofContext, group: CryptoGroup) => void;
|
|
4
4
|
export declare const contextElements: (context: ProofContext) => (bigint | string | Uint8Array)[];
|
|
5
5
|
export declare const fixedPoint: (value: string) => Uint8Array;
|
|
6
|
-
export declare const fixedScalar: (value: bigint
|
|
6
|
+
export declare const fixedScalar: (value: bigint) => Uint8Array;
|
|
7
7
|
export declare const hashChallenge: (payload: Uint8Array, q: bigint) => Promise<bigint>;
|
|
8
8
|
export declare const sumChallenges: (values: readonly bigint[], q: bigint) => bigint;
|
package/dist/proofs/helpers.js
CHANGED
|
@@ -59,9 +59,6 @@ export const contextElements = (context) => {
|
|
|
59
59
|
return fields;
|
|
60
60
|
};
|
|
61
61
|
export const fixedPoint = (value) => hexToBytes(value);
|
|
62
|
-
export const fixedScalar = (value
|
|
63
|
-
void group;
|
|
64
|
-
return hexToBytes(encodeScalar(value));
|
|
65
|
-
};
|
|
62
|
+
export const fixedScalar = (value) => hexToBytes(encodeScalar(value));
|
|
66
63
|
export const hashChallenge = (payload, q) => Promise.resolve(modQ(hashChallengeToScalar(payload), q));
|
|
67
64
|
export const sumChallenges = (values, q) => values.reduce((sum, value) => modQ(sum + value, q), 0n);
|
package/dist/proofs/types.d.ts
CHANGED
|
@@ -58,8 +58,8 @@ export type DisjunctiveBranch = {
|
|
|
58
58
|
/**
|
|
59
59
|
* A disjunctive proof over an ordered set of valid plaintext values.
|
|
60
60
|
*
|
|
61
|
-
* Ballot payloads use this to prove that an encrypted
|
|
62
|
-
* allowed
|
|
61
|
+
* Ballot payloads use this to prove that an encrypted value came from the
|
|
62
|
+
* allowed manifest domain without revealing which value was chosen.
|
|
63
63
|
*/
|
|
64
64
|
export type DisjunctiveProof = {
|
|
65
65
|
readonly branches: readonly DisjunctiveBranch[];
|
|
@@ -19,7 +19,7 @@ export declare const signProtocolPayload: <TMessageType extends ProtocolMessageT
|
|
|
19
19
|
* Creates a signed `manifest-publication` payload.
|
|
20
20
|
*
|
|
21
21
|
* This is the first public payload in the supported ceremony and anchors the
|
|
22
|
-
*
|
|
22
|
+
* explicit manifest on the board.
|
|
23
23
|
*/
|
|
24
24
|
export declare const createManifestPublicationPayload: (privateKey: CryptoKey, input: {
|
|
25
25
|
readonly manifest: ElectionManifest;
|
|
@@ -30,7 +30,7 @@ export const signProtocolPayload = async (privateKey, payload) => {
|
|
|
30
30
|
* Creates a signed `manifest-publication` payload.
|
|
31
31
|
*
|
|
32
32
|
* This is the first public payload in the supported ceremony and anchors the
|
|
33
|
-
*
|
|
33
|
+
* explicit manifest on the board.
|
|
34
34
|
*/
|
|
35
35
|
export const createManifestPublicationPayload = async (privateKey, input) => signProtocolPayload(privateKey, {
|
|
36
36
|
protocolVersion: input.protocolVersion,
|
|
@@ -25,12 +25,12 @@ export declare const assertSupportedProtocolVersion: (protocolVersion: string, l
|
|
|
25
25
|
* workflow.
|
|
26
26
|
*
|
|
27
27
|
* The manifest is intentionally minimal: it fixes the frozen roster hash and
|
|
28
|
-
* option list
|
|
29
|
-
* accepted registration roster.
|
|
28
|
+
* option list together with one explicit global score range, while participant
|
|
29
|
+
* count and threshold are derived later from the accepted registration roster.
|
|
30
30
|
*/
|
|
31
31
|
export declare const validateElectionManifest: (manifest: ElectionManifest) => ElectionManifest;
|
|
32
32
|
/**
|
|
33
|
-
* Creates the
|
|
33
|
+
* Creates the explicit election manifest after validating the supported
|
|
34
34
|
* invariants.
|
|
35
35
|
*/
|
|
36
36
|
export declare const createElectionManifest: (manifest: ElectionManifest) => ElectionManifest;
|
|
@@ -8,6 +8,7 @@ import { bytesToHex } from '../core/bytes.js';
|
|
|
8
8
|
import { InvalidPayloadError, sha256, utf8ToBytes } from '../core/index.js';
|
|
9
9
|
import { encodeForChallenge } from '../serialize/encoding.js';
|
|
10
10
|
import { canonicalizeJson } from './canonical-json.js';
|
|
11
|
+
import { validateSupportedScoreRange } from './score-range.js';
|
|
11
12
|
/**
|
|
12
13
|
* Default protocol namespace used by the built-in helpers and verifier.
|
|
13
14
|
*
|
|
@@ -20,6 +21,23 @@ const assertNonEmptyString = (value, label) => {
|
|
|
20
21
|
throw new InvalidPayloadError(`${label} must be a non-empty string`);
|
|
21
22
|
}
|
|
22
23
|
};
|
|
24
|
+
const validateScoreRange = (scoreRange) => {
|
|
25
|
+
if (typeof scoreRange !== 'object' ||
|
|
26
|
+
scoreRange === null ||
|
|
27
|
+
Array.isArray(scoreRange)) {
|
|
28
|
+
throw new InvalidPayloadError('Election manifest scoreRange must be an object with min and max bounds');
|
|
29
|
+
}
|
|
30
|
+
const scoreRangeRecord = scoreRange;
|
|
31
|
+
if (typeof scoreRangeRecord.min !== 'number' ||
|
|
32
|
+
typeof scoreRangeRecord.max !== 'number') {
|
|
33
|
+
throw new InvalidPayloadError('Election manifest scoreRange requires numeric min and max bounds');
|
|
34
|
+
}
|
|
35
|
+
return validateSupportedScoreRange(scoreRange, {
|
|
36
|
+
comparisonMax: 'scoreRange.max',
|
|
37
|
+
min: 'Election manifest scoreRange.min',
|
|
38
|
+
max: 'Election manifest scoreRange.max',
|
|
39
|
+
});
|
|
40
|
+
};
|
|
23
41
|
/**
|
|
24
42
|
* Validates that a protocol version string is present and non-empty.
|
|
25
43
|
*
|
|
@@ -48,12 +66,15 @@ export const assertSupportedProtocolVersion = (protocolVersion, label = 'Protoco
|
|
|
48
66
|
* workflow.
|
|
49
67
|
*
|
|
50
68
|
* The manifest is intentionally minimal: it fixes the frozen roster hash and
|
|
51
|
-
* option list
|
|
52
|
-
* accepted registration roster.
|
|
69
|
+
* option list together with one explicit global score range, while participant
|
|
70
|
+
* count and threshold are derived later from the accepted registration roster.
|
|
53
71
|
*/
|
|
54
72
|
export const validateElectionManifest = (manifest) => {
|
|
55
73
|
const manifestRecord = manifest;
|
|
56
74
|
assertNonEmptyString(manifest.rosterHash, 'Roster hash');
|
|
75
|
+
if (!('scoreRange' in manifestRecord)) {
|
|
76
|
+
throw new InvalidPayloadError('Election manifest requires an explicit scoreRange');
|
|
77
|
+
}
|
|
57
78
|
for (const legacyField of [
|
|
58
79
|
'participantCount',
|
|
59
80
|
'reconstructionThreshold',
|
|
@@ -71,7 +92,7 @@ export const validateElectionManifest = (manifest) => {
|
|
|
71
92
|
'requiresAllOptions',
|
|
72
93
|
]) {
|
|
73
94
|
if (legacyField in manifestRecord) {
|
|
74
|
-
throw new InvalidPayloadError(`Legacy manifest field "${legacyField}" is not supported
|
|
95
|
+
throw new InvalidPayloadError(`Legacy manifest field "${legacyField}" is not supported by the public manifest`);
|
|
75
96
|
}
|
|
76
97
|
}
|
|
77
98
|
if (manifest.optionList.length === 0) {
|
|
@@ -85,10 +106,13 @@ export const validateElectionManifest = (manifest) => {
|
|
|
85
106
|
}
|
|
86
107
|
seenOptions.add(option);
|
|
87
108
|
}
|
|
88
|
-
return
|
|
109
|
+
return {
|
|
110
|
+
...manifest,
|
|
111
|
+
scoreRange: validateScoreRange(manifest.scoreRange),
|
|
112
|
+
};
|
|
89
113
|
};
|
|
90
114
|
/**
|
|
91
|
-
* Creates the
|
|
115
|
+
* Creates the explicit election manifest after validating the supported
|
|
92
116
|
* invariants.
|
|
93
117
|
*/
|
|
94
118
|
export const createElectionManifest = (manifest) => validateElectionManifest(manifest);
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Low-level protocol helpers for transcript hashing, generic signed payloads,
|
|
3
|
-
*
|
|
3
|
+
* registration-roster hashing, signature verification, ballot proof
|
|
4
|
+
* verification, and protocol payload types.
|
|
4
5
|
*
|
|
5
|
-
* Use this module when you
|
|
6
|
-
*
|
|
6
|
+
* Use this module when you want protocol helpers grouped by subsystem instead
|
|
7
|
+
* of importing them from the root package.
|
|
7
8
|
*
|
|
8
9
|
* @module threshold-elgamal/protocol
|
|
9
10
|
* @packageDocumentation
|
|
10
11
|
*/
|
|
11
12
|
export { signProtocolPayload } from './builders.js';
|
|
12
13
|
export { hashProtocolTranscript } from './transcript.js';
|
|
14
|
+
export { hashRosterEntries, verifySignedProtocolPayloads, type RosterEntry, type VerifiedProtocolSignatures, } from './verification.js';
|
|
13
15
|
export { verifyBallotSubmissionPayloadsByOption } from './voting-ballots.js';
|
|
14
|
-
export {
|
|
15
|
-
export type { EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof, ProtocolMessageType, ProtocolPayload, } from './types.js';
|
|
16
|
+
export { scoreRangeDomain } from './voting-codecs.js';
|
|
17
|
+
export type { EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof, ProtocolMessageType, ProtocolPayload, ScoreRange, } from './types.js';
|
package/dist/protocol/public.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Low-level protocol helpers for transcript hashing, generic signed payloads,
|
|
3
|
-
*
|
|
3
|
+
* registration-roster hashing, signature verification, ballot proof
|
|
4
|
+
* verification, and protocol payload types.
|
|
4
5
|
*
|
|
5
|
-
* Use this module when you
|
|
6
|
-
*
|
|
6
|
+
* Use this module when you want protocol helpers grouped by subsystem instead
|
|
7
|
+
* of importing them from the root package.
|
|
7
8
|
*
|
|
8
9
|
* @module threshold-elgamal/protocol
|
|
9
10
|
* @packageDocumentation
|
|
10
11
|
*/
|
|
11
12
|
export { signProtocolPayload } from './builders.js';
|
|
12
13
|
export { hashProtocolTranscript } from './transcript.js';
|
|
14
|
+
export { hashRosterEntries, verifySignedProtocolPayloads, } from './verification.js';
|
|
13
15
|
export { verifyBallotSubmissionPayloadsByOption } from './voting-ballots.js';
|
|
14
|
-
export {
|
|
16
|
+
export { scoreRangeDomain } from './voting-codecs.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { InvalidPayloadError } from '../core/index.js';
|
|
2
|
+
const MAX_SUPPORTED_SCORE_RANGE_MAX = 100;
|
|
3
|
+
const assertSafeInteger = (value, label) => {
|
|
4
|
+
if (!Number.isSafeInteger(value)) {
|
|
5
|
+
throw new InvalidPayloadError(`${label} must be a safe integer`);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
export const validateSupportedScoreRange = (scoreRange, labels) => {
|
|
9
|
+
assertSafeInteger(scoreRange.min, labels.min);
|
|
10
|
+
assertSafeInteger(scoreRange.max, labels.max);
|
|
11
|
+
if (scoreRange.min < 0) {
|
|
12
|
+
throw new InvalidPayloadError(`${labels.min} must be non-negative`);
|
|
13
|
+
}
|
|
14
|
+
if (scoreRange.max < 0) {
|
|
15
|
+
throw new InvalidPayloadError(`${labels.max} must be non-negative`);
|
|
16
|
+
}
|
|
17
|
+
if (scoreRange.min > scoreRange.max) {
|
|
18
|
+
throw new InvalidPayloadError(`${labels.min} must not exceed ${labels.comparisonMax ?? labels.max}`);
|
|
19
|
+
}
|
|
20
|
+
if (scoreRange.max > MAX_SUPPORTED_SCORE_RANGE_MAX) {
|
|
21
|
+
throw new InvalidPayloadError(`${labels.max} must not exceed ${MAX_SUPPORTED_SCORE_RANGE_MAX}`);
|
|
22
|
+
}
|
|
23
|
+
return scoreRange;
|
|
24
|
+
};
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -136,7 +136,7 @@ export type FeldmanCommitmentPayload = BaseProtocolPayload & {
|
|
|
136
136
|
}[];
|
|
137
137
|
};
|
|
138
138
|
/**
|
|
139
|
-
* Final
|
|
139
|
+
* Final participant confirmation payload for the derived joint key.
|
|
140
140
|
*/
|
|
141
141
|
export type KeyDerivationConfirmation = BaseProtocolPayload & {
|
|
142
142
|
readonly messageType: 'key-derivation-confirmation';
|
|
@@ -217,12 +217,24 @@ export type SignedPayload<TPayload extends ProtocolPayload = ProtocolPayload> =
|
|
|
217
217
|
/**
|
|
218
218
|
* Canonical election-manifest shape bound into protocol transcripts.
|
|
219
219
|
*
|
|
220
|
-
* The manifest is intentionally
|
|
221
|
-
*
|
|
220
|
+
* The manifest is intentionally compact: it fixes the frozen roster hash,
|
|
221
|
+
* option list, and one explicit global score range, while participant count
|
|
222
|
+
* and threshold are derived later from the accepted registration roster.
|
|
223
|
+
*/
|
|
224
|
+
export type ScoreRange = {
|
|
225
|
+
readonly min: number;
|
|
226
|
+
readonly max: number;
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Canonical election-manifest shape bound into protocol transcripts.
|
|
230
|
+
*
|
|
231
|
+
* The score range applies uniformly to every option slot in the supported
|
|
232
|
+
* workflow.
|
|
222
233
|
*/
|
|
223
234
|
export type ElectionManifest = {
|
|
224
235
|
readonly rosterHash: string;
|
|
225
236
|
readonly optionList: readonly string[];
|
|
237
|
+
readonly scoreRange: ScoreRange;
|
|
226
238
|
};
|
|
227
239
|
/**
|
|
228
240
|
* Input bundle for verifying typed ballot payloads.
|
|
@@ -267,14 +279,15 @@ export type VerifyDecryptionSharePayloadsByOptionInput = {
|
|
|
267
279
|
* Input bundle for full ceremony verification across all published options.
|
|
268
280
|
*
|
|
269
281
|
* This is the top-level verifier input that an auditor or bulletin-board
|
|
270
|
-
* reader supplies when replaying a full ceremony
|
|
282
|
+
* reader supplies when replaying a full ceremony from the published board,
|
|
283
|
+
* including the full ballot-close slot instead of a preselected close record.
|
|
271
284
|
*/
|
|
272
285
|
export type VerifyElectionCeremonyInput = {
|
|
273
286
|
readonly manifest: ElectionManifest;
|
|
274
287
|
readonly sessionId: string;
|
|
275
288
|
readonly dkgTranscript: readonly SignedPayload[];
|
|
276
289
|
readonly ballotPayloads: readonly SignedPayload<BallotSubmissionPayload>[];
|
|
277
|
-
readonly
|
|
290
|
+
readonly ballotClosePayloads: readonly SignedPayload<BallotClosePayload>[];
|
|
278
291
|
readonly decryptionSharePayloads: readonly SignedPayload<DecryptionSharePayload>[];
|
|
279
292
|
readonly tallyPublications?: readonly SignedPayload<TallyPublicationPayload>[];
|
|
280
293
|
};
|
|
@@ -4,8 +4,9 @@ import { type VerifiedOptionBallotAggregation } from './voting-ballot-aggregatio
|
|
|
4
4
|
* Verifies typed ballot-submission payloads and recomputes one aggregate tally
|
|
5
5
|
* ciphertext per manifest option.
|
|
6
6
|
*
|
|
7
|
-
* This is the public entry point for applications that already
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* This is the public entry point for applications that already collected
|
|
8
|
+
* signed ballot payloads and want the per-option verified ciphertext
|
|
9
|
+
* aggregates that feed threshold decryption. The helper re-audits the signed
|
|
10
|
+
* payloads before it decodes and aggregates them.
|
|
10
11
|
*/
|
|
11
12
|
export declare const verifyBallotSubmissionPayloadsByOption: (input: VerifyBallotSubmissionPayloadsByOptionInput) => Promise<readonly VerifiedOptionBallotAggregation[]>;
|
|
@@ -40,9 +40,10 @@ const verifyAuditedBallotSubmissionPayloadsByOption = async (input) => {
|
|
|
40
40
|
* Verifies typed ballot-submission payloads and recomputes one aggregate tally
|
|
41
41
|
* ciphertext per manifest option.
|
|
42
42
|
*
|
|
43
|
-
* This is the public entry point for applications that already
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* This is the public entry point for applications that already collected
|
|
44
|
+
* signed ballot payloads and want the per-option verified ciphertext
|
|
45
|
+
* aggregates that feed threshold decryption. The helper re-audits the signed
|
|
46
|
+
* payloads before it decodes and aggregates them.
|
|
46
47
|
*/
|
|
47
48
|
export const verifyBallotSubmissionPayloadsByOption = async (input) => {
|
|
48
49
|
const context = await buildVotingManifestContext(input.manifest, input.sessionId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ElGamalCiphertext } from '../elgamal/types.js';
|
|
2
2
|
import type { DLEQProof, DisjunctiveProof } from '../proofs/types.js';
|
|
3
|
-
import type { EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof } from './types.js';
|
|
3
|
+
import type { EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof, ScoreRange } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Encodes an additive ciphertext into fixed-width protocol hex.
|
|
6
6
|
*
|
|
@@ -47,8 +47,11 @@ export declare const encodeDisjunctiveProof: (proof: DisjunctiveProof) => Encode
|
|
|
47
47
|
*/
|
|
48
48
|
export declare const decodeDisjunctiveProof: (proof: EncodedDisjunctiveProof) => DisjunctiveProof;
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* Expands one inclusive contiguous score range into its allowed plaintext
|
|
51
|
+
* domain.
|
|
51
52
|
*
|
|
52
|
-
*
|
|
53
|
+
* Ballot builders and verifiers pass the resulting values into the
|
|
54
|
+
* disjunctive-proof layer so each encrypted score can be proven to belong to
|
|
55
|
+
* the manifest-declared domain.
|
|
53
56
|
*/
|
|
54
|
-
export declare const
|
|
57
|
+
export declare const scoreRangeDomain: (scoreRange: ScoreRange) => readonly bigint[];
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* their published protocol payload representations.
|
|
4
4
|
*/
|
|
5
5
|
import { decodePoint, decodeScalar, encodeScalar } from '../core/ristretto.js';
|
|
6
|
+
import { validateSupportedScoreRange } from './score-range.js';
|
|
6
7
|
/**
|
|
7
8
|
* Encodes an additive ciphertext into fixed-width protocol hex.
|
|
8
9
|
*
|
|
@@ -66,8 +67,17 @@ export const decodeDisjunctiveProof = (proof) => ({
|
|
|
66
67
|
branches: proof.branches.map((branch) => decodeCompactProof(branch)),
|
|
67
68
|
});
|
|
68
69
|
/**
|
|
69
|
-
*
|
|
70
|
+
* Expands one inclusive contiguous score range into its allowed plaintext
|
|
71
|
+
* domain.
|
|
70
72
|
*
|
|
71
|
-
*
|
|
73
|
+
* Ballot builders and verifiers pass the resulting values into the
|
|
74
|
+
* disjunctive-proof layer so each encrypted score can be proven to belong to
|
|
75
|
+
* the manifest-declared domain.
|
|
72
76
|
*/
|
|
73
|
-
export const
|
|
77
|
+
export const scoreRangeDomain = (scoreRange) => {
|
|
78
|
+
validateSupportedScoreRange(scoreRange, {
|
|
79
|
+
min: 'Score range min',
|
|
80
|
+
max: 'Score range max',
|
|
81
|
+
});
|
|
82
|
+
return Object.freeze(Array.from({ length: scoreRange.max - scoreRange.min + 1 }, (_value, index) => BigInt(scoreRange.min + index)));
|
|
83
|
+
};
|
|
@@ -5,6 +5,7 @@ import type { VerifiedOptionDecryptionShares, VerifyDecryptionSharePayloadsByOpt
|
|
|
5
5
|
*
|
|
6
6
|
* This is the public entry point for applications that have already accepted a
|
|
7
7
|
* DKG transcript and verified ballot aggregates and now need to validate the
|
|
8
|
-
* published threshold shares.
|
|
8
|
+
* published threshold shares. The helper re-audits the signed share payloads
|
|
9
|
+
* before it groups and verifies them.
|
|
9
10
|
*/
|
|
10
11
|
export declare const verifyDecryptionSharePayloadsByOption: (input: VerifyDecryptionSharePayloadsByOptionInput) => Promise<readonly VerifiedOptionDecryptionShares[]>;
|
|
@@ -98,7 +98,8 @@ const verifyAuditedDecryptionSharePayloadsByOption = async (input) => {
|
|
|
98
98
|
*
|
|
99
99
|
* This is the public entry point for applications that have already accepted a
|
|
100
100
|
* DKG transcript and verified ballot aggregates and now need to validate the
|
|
101
|
-
* published threshold shares.
|
|
101
|
+
* published threshold shares. The helper re-audits the signed share payloads
|
|
102
|
+
* before it groups and verifies them.
|
|
102
103
|
*/
|
|
103
104
|
export const verifyDecryptionSharePayloadsByOption = async (input) => {
|
|
104
105
|
const context = await buildVotingManifestContext(input.manifest, input.sessionId);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProofContext } from '../proofs/types.js';
|
|
2
|
-
import type { DecryptionSharePayload, ElectionManifest,
|
|
2
|
+
import type { DecryptionSharePayload, ElectionManifest, RegistrationPayload, ProtocolPayload, SignedPayload, OptionAggregateInput } from './types.js';
|
|
3
3
|
export declare const BALLOT_SUBMISSION_PHASE = 5;
|
|
4
4
|
export declare const BALLOT_CLOSE_PHASE = 6;
|
|
5
5
|
export declare const DECRYPTION_SHARE_PHASE = 7;
|
|
@@ -10,6 +10,7 @@ type VotingManifestContext = {
|
|
|
10
10
|
readonly optionCount: number;
|
|
11
11
|
readonly protocolVersion: string;
|
|
12
12
|
readonly scoreDomainValues: readonly bigint[];
|
|
13
|
+
readonly scoreRangeMax: bigint;
|
|
13
14
|
readonly sessionId: string;
|
|
14
15
|
};
|
|
15
16
|
export declare const assertPhase: (payload: ProtocolPayload, expectedPhase: number, label: string) => void;
|
|
@@ -8,7 +8,7 @@ import { assertPositiveParticipantIndex, InvalidPayloadError, RISTRETTO_GROUP, }
|
|
|
8
8
|
import { importAuthPublicKey, verifyPayloadSignature } from '../transport/auth.js';
|
|
9
9
|
import { hashElectionManifest, assertSupportedProtocolVersion, SHIPPED_PROTOCOL_VERSION, validateElectionManifest, } from './manifest.js';
|
|
10
10
|
import { signedProtocolPayloadBytes } from './payloads.js';
|
|
11
|
-
import {
|
|
11
|
+
import { scoreRangeDomain } from './voting-codecs.js';
|
|
12
12
|
export const BALLOT_SUBMISSION_PHASE = 5;
|
|
13
13
|
export const BALLOT_CLOSE_PHASE = 6;
|
|
14
14
|
export const DECRYPTION_SHARE_PHASE = 7;
|
|
@@ -53,7 +53,8 @@ export const buildVotingManifestContext = async (manifest, sessionId) => {
|
|
|
53
53
|
manifestHash: await hashElectionManifest(validatedManifest),
|
|
54
54
|
optionCount: validatedManifest.optionList.length,
|
|
55
55
|
protocolVersion: SHIPPED_PROTOCOL_VERSION,
|
|
56
|
-
scoreDomainValues:
|
|
56
|
+
scoreDomainValues: scoreRangeDomain(validatedManifest.scoreRange),
|
|
57
|
+
scoreRangeMax: BigInt(validatedManifest.scoreRange.max),
|
|
57
58
|
sessionId,
|
|
58
59
|
};
|
|
59
60
|
};
|
|
@@ -66,8 +66,8 @@ export type ElectionVerificationResult = {
|
|
|
66
66
|
};
|
|
67
67
|
/**
|
|
68
68
|
* Replays the published ceremony from manifest to tally, including board audit,
|
|
69
|
-
* DKG verification, ballot
|
|
70
|
-
* per-option tally checks.
|
|
69
|
+
* DKG verification, full ballot-close-slot audit, ballot verification,
|
|
70
|
+
* decryption-share verification, and per-option tally checks.
|
|
71
71
|
*
|
|
72
72
|
* This is the main verifier entry point for callers that want failures to
|
|
73
73
|
* abort immediately.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This module is the shortest path for auditors and bulletin-board readers who
|
|
5
5
|
* want one call that replays manifest validation, board audit, DKG, ballots,
|
|
6
|
-
* decryption shares, and tally checks.
|
|
6
|
+
* the full ballot-close slot, decryption shares, and tally checks.
|
|
7
7
|
*/
|
|
8
8
|
import { InvalidPayloadError } from '../core/index.js';
|
|
9
9
|
import { decodeScalar } from '../core/ristretto.js';
|
|
@@ -37,7 +37,7 @@ const recomputePublishedTally = (input) => {
|
|
|
37
37
|
sessionId: input.sessionId,
|
|
38
38
|
optionIndex: input.ballots.optionIndex,
|
|
39
39
|
});
|
|
40
|
-
return combineDecryptionShares(preparedAggregate.ciphertext, input.decryptionShares.map((entry) => entry.share), BigInt(input.ballots.aggregate.ballotCount) *
|
|
40
|
+
return combineDecryptionShares(preparedAggregate.ciphertext, input.decryptionShares.map((entry) => entry.share), BigInt(input.ballots.aggregate.ballotCount) * input.scoreRangeMax);
|
|
41
41
|
};
|
|
42
42
|
const verifyPublishedTallyPayload = (payload, optionIndex, ballots, decryptionShares, tally) => {
|
|
43
43
|
if (payload.transcriptHash !== ballots.aggregate.transcriptHash) {
|
|
@@ -127,8 +127,8 @@ const verifyBallotClosePayload = (input) => {
|
|
|
127
127
|
};
|
|
128
128
|
/**
|
|
129
129
|
* Replays the published ceremony from manifest to tally, including board audit,
|
|
130
|
-
* DKG verification, ballot
|
|
131
|
-
* per-option tally checks.
|
|
130
|
+
* DKG verification, full ballot-close-slot audit, ballot verification,
|
|
131
|
+
* decryption-share verification, and per-option tally checks.
|
|
132
132
|
*
|
|
133
133
|
* This is the main verifier entry point for callers that want failures to
|
|
134
134
|
* abort immediately.
|
|
@@ -150,9 +150,7 @@ export const verifyElectionCeremony = async (input) => {
|
|
|
150
150
|
try {
|
|
151
151
|
dkgAudit = await auditSignedPayloads(input.dkgTranscript);
|
|
152
152
|
ballotAudit = await auditSignedPayloads(input.ballotPayloads);
|
|
153
|
-
ballotCloseAudit = await auditSignedPayloads(
|
|
154
|
-
input.ballotClosePayload,
|
|
155
|
-
]);
|
|
153
|
+
ballotCloseAudit = await auditSignedPayloads(input.ballotClosePayloads);
|
|
156
154
|
decryptionAudit = await auditSignedPayloads(input.decryptionSharePayloads);
|
|
157
155
|
tallyAudit =
|
|
158
156
|
input.tallyPublications === undefined ||
|
|
@@ -280,6 +278,7 @@ export const verifyElectionCeremony = async (input) => {
|
|
|
280
278
|
jointPublicKey: dkg.jointPublicKey,
|
|
281
279
|
protocolVersion: context.protocolVersion,
|
|
282
280
|
manifestHash: context.manifestHash,
|
|
281
|
+
scoreRangeMax: context.scoreRangeMax,
|
|
283
282
|
sessionId: context.sessionId,
|
|
284
283
|
});
|
|
285
284
|
const publication = tallyPublicationMap.get(optionIndex);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "threshold-elgamal",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A browser-native TypeScript ElGamal library for Ristretto255-based research prototypes, shipping additive ElGamal, threshold decryption, protocol helpers, board auditing, transport primitives, and log-driven DKG state machines.",
|
|
5
5
|
"author": "Piotr Piech <piotr@piech.dev>",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"coverage:badge": "pnpm run coverage:node && tsx ./tools/generate-coverage-badge.ts",
|
|
27
27
|
"smoke:pack": "tsx ./tools/ci/verify-packed-package.ts",
|
|
28
28
|
"prebuild": "pnpm run check && pnpm run test",
|
|
29
|
-
"build": "pnpm exec del-cli dist && tsc --project tsconfig.build.json && tsx ./tools/build/rewrite-dist-relative-imports.ts",
|
|
29
|
+
"build:dist": "pnpm exec del-cli dist && tsc --project tsconfig.build.json && tsx ./tools/build/rewrite-dist-relative-imports.ts",
|
|
30
|
+
"build": "pnpm run build:dist",
|
|
30
31
|
"vectors:threshold": "tsx ./tools/generate-threshold-vectors.ts",
|
|
31
32
|
"vectors:protocol": "tsx ./tools/generate-protocol-vectors.ts",
|
|
32
33
|
"bench:micro": "tsx ./tools/benchmarks/microbench.ts",
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
"prepare": "husky"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
|
-
"@noble/curves": "^2.0
|
|
43
|
-
"@noble/hashes": "^2.0
|
|
43
|
+
"@noble/curves": "^2.2.0",
|
|
44
|
+
"@noble/hashes": "^2.2.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@astrojs/mdx": "^5.0.3",
|
|
@@ -49,25 +50,25 @@
|
|
|
49
50
|
"@eslint/js": "^10.0.1",
|
|
50
51
|
"@fontsource/ibm-plex-sans": "^5.2.8",
|
|
51
52
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
52
|
-
"@types/node": "^25.
|
|
53
|
-
"@typescript-eslint/eslint-plugin": "^8.58.
|
|
54
|
-
"@typescript-eslint/parser": "^8.58.
|
|
53
|
+
"@types/node": "^25.6.0",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
|
55
|
+
"@typescript-eslint/parser": "^8.58.2",
|
|
55
56
|
"@vitest/browser-playwright": "^4.1.4",
|
|
56
57
|
"@vitest/coverage-v8": "^4.1.4",
|
|
57
|
-
"astro": "^6.1.
|
|
58
|
+
"astro": "^6.1.7",
|
|
58
59
|
"del-cli": "^7.0.0",
|
|
59
60
|
"eslint": "^10.2.0",
|
|
60
61
|
"eslint-config-prettier": "^10.1.8",
|
|
61
62
|
"eslint-plugin-import-x": "^4.16.2",
|
|
62
63
|
"eslint-plugin-only-error": "^1.0.2",
|
|
63
64
|
"eslint-plugin-prettier": "^5.5.5",
|
|
64
|
-
"globals": "^17.
|
|
65
|
+
"globals": "^17.5.0",
|
|
65
66
|
"husky": "^9.1.7",
|
|
66
|
-
"knip": "^
|
|
67
|
+
"knip": "^6.4.1",
|
|
67
68
|
"playwright": "^1.59.1",
|
|
68
|
-
"prettier": "^3.8.
|
|
69
|
+
"prettier": "^3.8.3",
|
|
69
70
|
"tsx": "^4.21.0",
|
|
70
|
-
"typedoc": "^0.28.
|
|
71
|
+
"typedoc": "^0.28.19",
|
|
71
72
|
"typedoc-plugin-markdown": "^4.11.0",
|
|
72
73
|
"typescript": "^6.0.2",
|
|
73
74
|
"vite": "^8.0.8",
|