threshold-elgamal 1.0.0-beta.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +63 -12
  2. package/dist/core/bigint.d.ts +0 -13
  3. package/dist/core/bigint.js +1 -11
  4. package/dist/core/bytes.d.ts +1 -1
  5. package/dist/core/bytes.js +6 -3
  6. package/dist/core/errors.d.ts +12 -12
  7. package/dist/core/errors.js +12 -12
  8. package/dist/core/groups.d.ts +2 -7
  9. package/dist/core/groups.js +11 -10
  10. package/dist/core/ristretto.d.ts +2 -6
  11. package/dist/core/ristretto.js +1 -11
  12. package/dist/core/types.d.ts +0 -2
  13. package/dist/dkg/checkpoints.d.ts +8 -9
  14. package/dist/dkg/checkpoints.js +50 -12
  15. package/dist/dkg/pedersen-share-codec.js +65 -8
  16. package/dist/dkg/verification-complaints.d.ts +2 -2
  17. package/dist/dkg/verification-complaints.js +3 -3
  18. package/dist/dkg/verification.d.ts +74 -5
  19. package/dist/dkg/verification.js +371 -76
  20. package/dist/elgamal/additive.d.ts +15 -8
  21. package/dist/elgamal/additive.js +34 -30
  22. package/dist/elgamal/index.d.ts +0 -3
  23. package/dist/elgamal/index.js +0 -3
  24. package/dist/elgamal/types.d.ts +3 -7
  25. package/dist/index.d.ts +5 -5
  26. package/dist/index.js +3 -3
  27. package/dist/proofs/disjunctive.d.ts +3 -3
  28. package/dist/proofs/dleq.d.ts +2 -2
  29. package/dist/proofs/helpers.d.ts +0 -1
  30. package/dist/proofs/helpers.js +1 -3
  31. package/dist/protocol/board-audit.d.ts +5 -12
  32. package/dist/protocol/board-audit.js +7 -34
  33. package/dist/protocol/builders.d.ts +3 -3
  34. package/dist/protocol/builders.js +22 -20
  35. package/dist/protocol/canonical-json.d.ts +4 -3
  36. package/dist/protocol/index.d.ts +3 -4
  37. package/dist/protocol/index.js +3 -4
  38. package/dist/protocol/manifest.d.ts +0 -6
  39. package/dist/protocol/manifest.js +0 -6
  40. package/dist/protocol/payloads.js +0 -4
  41. package/dist/protocol/transcript.d.ts +0 -19
  42. package/dist/protocol/transcript.js +3 -4
  43. package/dist/protocol/types.d.ts +62 -25
  44. package/dist/protocol/verification.js +1 -1
  45. package/dist/protocol/{ballots.d.ts → voting-ballot-aggregation.d.ts} +6 -17
  46. package/dist/protocol/{ballots.js → voting-ballot-aggregation.js} +3 -3
  47. package/dist/protocol/{ballot-close.d.ts → voting-ballot-close.d.ts} +2 -1
  48. package/dist/protocol/{ballot-close.js → voting-ballot-close.js} +6 -6
  49. package/dist/protocol/voting-ballots.d.ts +2 -2
  50. package/dist/protocol/voting-ballots.js +24 -17
  51. package/dist/protocol/voting-codecs.d.ts +5 -8
  52. package/dist/protocol/voting-codecs.js +10 -19
  53. package/dist/protocol/voting-decryption.d.ts +1 -10
  54. package/dist/protocol/voting-decryption.js +23 -40
  55. package/dist/protocol/voting-shared.d.ts +4 -7
  56. package/dist/protocol/voting-shared.js +4 -10
  57. package/dist/protocol/{election-verification.d.ts → voting-verification.d.ts} +27 -17
  58. package/dist/protocol/{election-verification.js → voting-verification.js} +28 -7
  59. package/dist/serialize/encoding.d.ts +1 -1
  60. package/dist/serialize/encoding.js +5 -18
  61. package/dist/threshold/decrypt.d.ts +13 -9
  62. package/dist/threshold/decrypt.js +21 -15
  63. package/dist/threshold/index.d.ts +1 -4
  64. package/dist/threshold/index.js +1 -4
  65. package/dist/threshold/types.d.ts +3 -3
  66. package/dist/transport/auth.d.ts +1 -1
  67. package/dist/transport/auth.js +1 -1
  68. package/dist/transport/complaints.d.ts +0 -22
  69. package/dist/transport/complaints.js +1 -48
  70. package/dist/transport/envelopes.d.ts +2 -0
  71. package/dist/transport/envelopes.js +5 -3
  72. package/dist/transport/index.d.ts +1 -1
  73. package/dist/transport/index.js +1 -1
  74. package/dist/transport/key-agreement.d.ts +0 -6
  75. package/dist/transport/key-agreement.js +0 -18
  76. package/dist/vss/feldman.d.ts +2 -0
  77. package/dist/vss/feldman.js +13 -5
  78. package/dist/vss/pedersen.d.ts +2 -1
  79. package/dist/vss/pedersen.js +13 -7
  80. package/package.json +15 -29
  81. package/dist/core/group-invariants.d.ts +0 -2
  82. package/dist/core/group-invariants.js +0 -14
  83. package/dist/dkg/complaints.d.ts +0 -51
  84. package/dist/dkg/complaints.js +0 -129
  85. package/dist/dkg/gjkr.d.ts +0 -25
  86. package/dist/dkg/gjkr.js +0 -30
  87. package/dist/dkg/index.d.ts +0 -8
  88. package/dist/dkg/index.js +0 -8
  89. package/dist/dkg/majority-reducer.d.ts +0 -4
  90. package/dist/dkg/majority-reducer.js +0 -144
  91. package/dist/dkg/phase-plan.d.ts +0 -2
  92. package/dist/dkg/phase-plan.js +0 -21
  93. package/dist/dkg/reconstruction.d.ts +0 -9
  94. package/dist/dkg/reconstruction.js +0 -28
  95. package/dist/dkg/reducer-auth.d.ts +0 -3
  96. package/dist/dkg/reducer-auth.js +0 -87
  97. package/dist/dkg/types.d.ts +0 -33
  98. package/dist/dkg/types.js +0 -1
  99. package/dist/dkg/verification-checkpoints.d.ts +0 -5
  100. package/dist/dkg/verification-checkpoints.js +0 -40
  101. package/dist/dkg/verification-derivation.d.ts +0 -66
  102. package/dist/dkg/verification-derivation.js +0 -85
  103. package/dist/dkg/verification-feldman.d.ts +0 -8
  104. package/dist/dkg/verification-feldman.js +0 -80
  105. package/dist/dkg/verification-roster.d.ts +0 -5
  106. package/dist/dkg/verification-roster.js +0 -61
  107. package/dist/dkg/verification-shared.d.ts +0 -19
  108. package/dist/dkg/verification-shared.js +0 -98
  109. package/dist/dkg/verification-types.d.ts +0 -51
  110. package/dist/dkg/verification-types.js +0 -1
  111. package/dist/elgamal/ciphertext.d.ts +0 -5
  112. package/dist/elgamal/ciphertext.js +0 -13
  113. package/dist/elgamal/keygen.d.ts +0 -9
  114. package/dist/elgamal/keygen.js +0 -18
  115. package/dist/elgamal/validation.d.ts +0 -15
  116. package/dist/elgamal/validation.js +0 -27
  117. package/dist/protocol/voting-types.d.ts +0 -64
  118. package/dist/protocol/voting-types.js +0 -1
  119. package/dist/threshold/lagrange.d.ts +0 -9
  120. package/dist/threshold/lagrange.js +0 -21
  121. package/dist/threshold/polynomial.d.ts +0 -24
  122. package/dist/threshold/polynomial.js +0 -41
  123. package/dist/threshold/shares.d.ts +0 -6
  124. package/dist/threshold/shares.js +0 -25
  125. package/dist/transport/envelope-crypto.d.ts +0 -3
  126. package/dist/transport/envelope-crypto.js +0 -6
  127. package/dist/vss/commitment-product.d.ts +0 -2
  128. package/dist/vss/commitment-product.js +0 -14
package/README.md CHANGED
@@ -2,9 +2,15 @@
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/threshold-elgamal.svg)](https://www.npmjs.com/package/threshold-elgamal)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/threshold-elgamal)](https://www.npmjs.com/package/threshold-elgamal)
5
+
6
+ ---
7
+
5
8
  [![CI](https://img.shields.io/github/actions/workflow/status/Tenemo/threshold-elgamal/ci.yml?branch=master&label=passing%20tests)](https://github.com/Tenemo/threshold-elgamal/actions/workflows/ci.yml)
6
- [![Tests coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Tenemo/threshold-elgamal/master/docs/public/coverage-badge.json)](docs/public/coverage-summary.json)
9
+ [![Tests coverage](https://img.shields.io/endpoint?url=https://tenemo.github.io/threshold-elgamal/coverage-badge.json)](https://tenemo.github.io/threshold-elgamal/coverage-summary.json)
7
10
  [![Documentation build](https://img.shields.io/github/actions/workflow/status/Tenemo/threshold-elgamal/pages.yml?branch=master&label=docs)](https://github.com/Tenemo/threshold-elgamal/actions/workflows/pages.yml)
11
+
12
+ ---
13
+
8
14
  [![Node version](https://img.shields.io/badge/node-%E2%89%A524.14.1-5FA04E?logo=node.js&logoColor=white)](https://nodejs.org/)
9
15
  [![License](https://img.shields.io/github/license/Tenemo/threshold-elgamal)](LICENSE)
10
16
 
@@ -24,7 +30,7 @@ This is a hardened research prototype. It has not been audited.
24
30
  ## Installation
25
31
 
26
32
  ```bash
27
- pnpm add threshold-elgamal
33
+ npm install threshold-elgamal
28
34
  ```
29
35
 
30
36
  ## Runtime requirements
@@ -35,6 +41,18 @@ pnpm add threshold-elgamal
35
41
  - Authentication signatures require Web Crypto `Ed25519`.
36
42
  - Transport share exchange requires Web Crypto `X25519`.
37
43
 
44
+ ## Documentation
45
+
46
+ - Hosted documentation site: [tenemo.github.io/threshold-elgamal](https://tenemo.github.io/threshold-elgamal/)
47
+ - Get started: [tenemo.github.io/threshold-elgamal/guides/getting-started](https://tenemo.github.io/threshold-elgamal/guides/getting-started/)
48
+ - Verifying a public board: [tenemo.github.io/threshold-elgamal/guides/verifying-a-public-board](https://tenemo.github.io/threshold-elgamal/guides/verifying-a-public-board/)
49
+ - Browser and worker usage: [tenemo.github.io/threshold-elgamal/guides/browser-and-worker-usage](https://tenemo.github.io/threshold-elgamal/guides/browser-and-worker-usage/)
50
+ - Published payload examples: [tenemo.github.io/threshold-elgamal/guides/published-payload-examples](https://tenemo.github.io/threshold-elgamal/guides/published-payload-examples/)
51
+ - Honest-majority voting flow: [tenemo.github.io/threshold-elgamal/guides/three-participant-voting-flow](https://tenemo.github.io/threshold-elgamal/guides/three-participant-voting-flow/)
52
+ - Security boundary: [tenemo.github.io/threshold-elgamal/guides/security-and-non-goals](https://tenemo.github.io/threshold-elgamal/guides/security-and-non-goals/)
53
+ - Production voting safety review: [tenemo.github.io/threshold-elgamal/guides/production-voting-safety-review](https://tenemo.github.io/threshold-elgamal/guides/production-voting-safety-review/)
54
+ - API docs: [tenemo.github.io/threshold-elgamal/api](https://tenemo.github.io/threshold-elgamal/api/)
55
+
38
56
  ## Browser support
39
57
 
40
58
  The shipped cryptographic browser path is fixed:
@@ -62,7 +80,7 @@ The supported boardroom flow is:
62
80
  5. Post ballot payloads for complete `1..10` score ballots.
63
81
  6. Post one organizer-signed `ballot-close` payload that freezes which complete ballots are counted.
64
82
  7. Post threshold decryption shares and tally publications for the close-selected ballot set.
65
- 8. Verify the whole ceremony with `verifyElectionCeremonyDetailed(...)`.
83
+ 8. Verify the whole ceremony with `verifyElectionCeremony(...)`.
66
84
 
67
85
  The cryptographic threshold is derived internally from the accepted registration roster:
68
86
 
@@ -72,6 +90,14 @@ The cryptographic threshold is derived internally from the accepted registration
72
90
 
73
91
  There is no supported `n-of-n` mode and no supported public `k-of-n` configuration.
74
92
 
93
+ Transcript verification requires key-derivation confirmations from every qualified participant.
94
+
95
+ ## Choose your entry point
96
+
97
+ - Verifying a public board: start with the hosted guide for `tryVerifyElectionCeremony(...)` and `verifyElectionCeremony(...)`.
98
+ - Browser and worker usage: start with the browser guide for key generation, manifest setup, and encrypted transport envelopes.
99
+ - Payload shapes and storage: start with the payload examples guide if you need concrete JSON, posting, or persistence patterns.
100
+
75
101
  ## Getting started
76
102
 
77
103
  ```typescript
@@ -118,6 +144,34 @@ console.log(majorityThreshold(3)); // 2
118
144
  console.log(sessionId.length); // 64
119
145
  ```
120
146
 
147
+ If your application consumes a complete public board, the shortest safe verifier entry point is:
148
+
149
+ ```typescript
150
+ import {
151
+ tryVerifyElectionCeremony,
152
+ type VerifyElectionCeremonyInput,
153
+ } from "threshold-elgamal";
154
+
155
+ const bundle: VerifyElectionCeremonyInput = {
156
+ manifest,
157
+ sessionId,
158
+ dkgTranscript,
159
+ ballotPayloads,
160
+ ballotClosePayload,
161
+ decryptionSharePayloads,
162
+ tallyPublications,
163
+ };
164
+
165
+ const result = await tryVerifyElectionCeremony(bundle);
166
+
167
+ if (!result.ok) {
168
+ console.error(result.error.stage, result.error.code, result.error.reason);
169
+ } else {
170
+ console.log(result.verified.perOptionTallies);
171
+ console.log(result.verified.boardAudit.overall.fingerprint);
172
+ }
173
+ ```
174
+
121
175
  The root package also exposes public builders for:
122
176
 
123
177
  - manifest publication
@@ -132,7 +186,7 @@ The root package also exposes public builders for:
132
186
  - decryption shares
133
187
  - tally publication
134
188
 
135
- For a full executable ceremony example, use the public node integration tests in this repository.
189
+ For concrete integration examples, start with the hosted guides below. The repository integration harness exercises the same workflow, but it is not part of the supported public API.
136
190
 
137
191
  ## Security boundary
138
192
 
@@ -157,19 +211,16 @@ What it does not claim:
157
211
 
158
212
  `ballot-close` is an auditable administrative cutoff, not a fairness proof about board arrival order. The library proves what was counted, not whether the organizer waited long enough before closing.
159
213
 
160
- ## Documentation
161
-
162
- - Hosted documentation site: [tenemo.github.io/threshold-elgamal](https://tenemo.github.io/threshold-elgamal/)
163
- - Get started: [tenemo.github.io/threshold-elgamal/guides/getting-started](https://tenemo.github.io/threshold-elgamal/guides/getting-started/)
164
- - Honest-majority voting flow: [tenemo.github.io/threshold-elgamal/guides/three-participant-voting-flow](https://tenemo.github.io/threshold-elgamal/guides/three-participant-voting-flow/)
165
- - Security boundary: [tenemo.github.io/threshold-elgamal/guides/security-and-non-goals](https://tenemo.github.io/threshold-elgamal/guides/security-and-non-goals/)
166
- - API docs: [tenemo.github.io/threshold-elgamal/api](https://tenemo.github.io/threshold-elgamal/api/)
214
+ For a production-threat-model verdict that maps these boundaries to the shipped verifier and tests, read the production voting safety review in the hosted docs.
167
215
 
168
216
  ## Development
169
217
 
170
218
  ```bash
171
219
  pnpm install
172
- pnpm run ci
220
+ pnpm run lint
221
+ pnpm run tsc
222
+ pnpm run test
223
+ pnpm run build
173
224
  ```
174
225
 
175
226
  ## License
@@ -4,25 +4,12 @@
4
4
  * @throws {@link InvalidScalarError} When `modulus` is not positive.
5
5
  */
6
6
  export declare const mod: (value: bigint, modulus: bigint) => bigint;
7
- /**
8
- * Reduces a value into the range `0..p-1`.
9
- *
10
- * @throws {@link InvalidScalarError} When `p` is not positive.
11
- */
12
- export declare const modP: (value: bigint, p: bigint) => bigint;
13
7
  /**
14
8
  * Reduces a value into the range `0..q-1`.
15
9
  *
16
10
  * @throws {@link InvalidScalarError} When `q` is not positive.
17
11
  */
18
12
  export declare const modQ: (value: bigint, q: bigint) => bigint;
19
- /**
20
- * Computes the multiplicative inverse of a value modulo `p`.
21
- *
22
- * @throws {@link InvalidScalarError} When `p` is not positive or the inverse
23
- * does not exist.
24
- */
25
- export declare const modInvP: (value: bigint, p: bigint) => bigint;
26
13
  /**
27
14
  * Computes the multiplicative inverse of a value modulo `q`.
28
15
  *
@@ -62,23 +62,13 @@ export const mod = (value, modulus) => {
62
62
  *
63
63
  * @throws {@link InvalidScalarError} When `p` is not positive.
64
64
  */
65
- export const modP = (value, p) => mod(value, p);
65
+ const modP = (value, p) => mod(value, p);
66
66
  /**
67
67
  * Reduces a value into the range `0..q-1`.
68
68
  *
69
69
  * @throws {@link InvalidScalarError} When `q` is not positive.
70
70
  */
71
71
  export const modQ = (value, q) => mod(value, q);
72
- /**
73
- * Computes the multiplicative inverse of a value modulo `p`.
74
- *
75
- * @throws {@link InvalidScalarError} When `p` is not positive or the inverse
76
- * does not exist.
77
- */
78
- export const modInvP = (value, p) => {
79
- assertPositiveModulus(p);
80
- return modInv(modP(value, p), p);
81
- };
82
72
  /**
83
73
  * Computes the multiplicative inverse of a value modulo `q`.
84
74
  *
@@ -1,5 +1,5 @@
1
1
  export declare const bytesToHex: (bytes: Uint8Array) => string;
2
- export declare const hexToBytes: (hex: string) => Uint8Array;
2
+ export declare const hexToBytes: (hex: string, errorMessage?: string) => Uint8Array;
3
3
  export declare const bytesToBigInt: (bytes: Uint8Array) => bigint;
4
4
  export declare const bytesToBigIntLE: (bytes: Uint8Array) => bigint;
5
5
  export declare const toBufferSource: (bytes: Uint8Array) => ArrayBuffer;
@@ -1,3 +1,6 @@
1
+ import { InvalidPayloadError } from './errors.js';
2
+ const hexPattern = /^[0-9a-f]+$/i;
3
+ const defaultHexErrorMessage = 'Hex input must be a non-empty even-length hexadecimal string';
1
4
  export const bytesToHex = (bytes) => {
2
5
  let hex = '';
3
6
  for (const byte of bytes) {
@@ -5,9 +8,9 @@ export const bytesToHex = (bytes) => {
5
8
  }
6
9
  return hex;
7
10
  };
8
- export const hexToBytes = (hex) => {
9
- if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) {
10
- throw new Error('Hex input must be a non-empty even-length string');
11
+ export const hexToBytes = (hex, errorMessage = defaultHexErrorMessage) => {
12
+ if (hex.length === 0 || hex.length % 2 !== 0 || !hexPattern.test(hex)) {
13
+ throw new InvalidPayloadError(errorMessage);
11
14
  }
12
15
  const bytes = new Uint8Array(hex.length / 2);
13
16
  for (let index = 0; index < hex.length; index += 2) {
@@ -1,37 +1,37 @@
1
- declare class ThresholdElgamalError extends Error {
1
+ declare class ThresholdElGamalError extends Error {
2
2
  constructor(message: string);
3
3
  }
4
4
  /** Raised when a scalar value falls outside the expected mathematical domain. */
5
- export declare class InvalidScalarError extends ThresholdElgamalError {
5
+ export declare class InvalidScalarError extends ThresholdElGamalError {
6
6
  }
7
7
  /** Raised when a group element is not valid for the selected suite. */
8
- export declare class InvalidGroupElementError extends ThresholdElgamalError {
8
+ export declare class InvalidGroupElementError extends ThresholdElGamalError {
9
9
  }
10
10
  /** Raised when a participant index falls outside the valid `1..n` range. */
11
- export declare class IndexOutOfRangeError extends ThresholdElgamalError {
11
+ export declare class IndexOutOfRangeError extends ThresholdElGamalError {
12
12
  }
13
13
  /** Raised when serialized payload bytes do not satisfy the required encoding. */
14
- export declare class InvalidPayloadError extends ThresholdElgamalError {
14
+ export declare class InvalidPayloadError extends ThresholdElGamalError {
15
15
  }
16
16
  /** Raised when a proof transcript or response fails verification. */
17
- export declare class InvalidProofError extends ThresholdElgamalError {
17
+ export declare class InvalidProofError extends ThresholdElGamalError {
18
18
  }
19
19
  /** Raised when the requested suite or runtime capability is unavailable. */
20
- export declare class UnsupportedSuiteError extends ThresholdElgamalError {
20
+ export declare class UnsupportedSuiteError extends ThresholdElGamalError {
21
21
  }
22
22
  /** Raised when a plaintext lies outside the allowed domain for the chosen mode. */
23
- export declare class PlaintextDomainError extends ThresholdElgamalError {
23
+ export declare class PlaintextDomainError extends ThresholdElGamalError {
24
24
  }
25
25
  /** Raised when a serialized or reconstructed share fails validation. */
26
- export declare class InvalidShareError extends ThresholdElgamalError {
26
+ export declare class InvalidShareError extends ThresholdElGamalError {
27
27
  }
28
28
  /** Raised when a protocol step transition violates the state machine rules. */
29
- export declare class PhaseViolationError extends ThresholdElgamalError {
29
+ export declare class PhaseViolationError extends ThresholdElGamalError {
30
30
  }
31
31
  /** Raised when threshold parameters do not satisfy `1 <= k <= n`. */
32
- export declare class ThresholdViolationError extends ThresholdElgamalError {
32
+ export declare class ThresholdViolationError extends ThresholdElGamalError {
33
33
  }
34
34
  /** Raised when transcript hashes or canonical bytes do not match expectations. */
35
- export declare class TranscriptMismatchError extends ThresholdElgamalError {
35
+ export declare class TranscriptMismatchError extends ThresholdElGamalError {
36
36
  }
37
37
  export {};
@@ -1,4 +1,4 @@
1
- class ThresholdElgamalError extends Error {
1
+ class ThresholdElGamalError extends Error {
2
2
  constructor(message) {
3
3
  super(message);
4
4
  this.name = new.target.name;
@@ -6,35 +6,35 @@ class ThresholdElgamalError extends Error {
6
6
  }
7
7
  }
8
8
  /** Raised when a scalar value falls outside the expected mathematical domain. */
9
- export class InvalidScalarError extends ThresholdElgamalError {
9
+ export class InvalidScalarError extends ThresholdElGamalError {
10
10
  }
11
11
  /** Raised when a group element is not valid for the selected suite. */
12
- export class InvalidGroupElementError extends ThresholdElgamalError {
12
+ export class InvalidGroupElementError extends ThresholdElGamalError {
13
13
  }
14
14
  /** Raised when a participant index falls outside the valid `1..n` range. */
15
- export class IndexOutOfRangeError extends ThresholdElgamalError {
15
+ export class IndexOutOfRangeError extends ThresholdElGamalError {
16
16
  }
17
17
  /** Raised when serialized payload bytes do not satisfy the required encoding. */
18
- export class InvalidPayloadError extends ThresholdElgamalError {
18
+ export class InvalidPayloadError extends ThresholdElGamalError {
19
19
  }
20
20
  /** Raised when a proof transcript or response fails verification. */
21
- export class InvalidProofError extends ThresholdElgamalError {
21
+ export class InvalidProofError extends ThresholdElGamalError {
22
22
  }
23
23
  /** Raised when the requested suite or runtime capability is unavailable. */
24
- export class UnsupportedSuiteError extends ThresholdElgamalError {
24
+ export class UnsupportedSuiteError extends ThresholdElGamalError {
25
25
  }
26
26
  /** Raised when a plaintext lies outside the allowed domain for the chosen mode. */
27
- export class PlaintextDomainError extends ThresholdElgamalError {
27
+ export class PlaintextDomainError extends ThresholdElGamalError {
28
28
  }
29
29
  /** Raised when a serialized or reconstructed share fails validation. */
30
- export class InvalidShareError extends ThresholdElgamalError {
30
+ export class InvalidShareError extends ThresholdElGamalError {
31
31
  }
32
32
  /** Raised when a protocol step transition violates the state machine rules. */
33
- export class PhaseViolationError extends ThresholdElgamalError {
33
+ export class PhaseViolationError extends ThresholdElGamalError {
34
34
  }
35
35
  /** Raised when threshold parameters do not satisfy `1 <= k <= n`. */
36
- export class ThresholdViolationError extends ThresholdElgamalError {
36
+ export class ThresholdViolationError extends ThresholdElGamalError {
37
37
  }
38
38
  /** Raised when transcript hashes or canonical bytes do not match expectations. */
39
- export class TranscriptMismatchError extends ThresholdElgamalError {
39
+ export class TranscriptMismatchError extends ThresholdElGamalError {
40
40
  }
@@ -1,9 +1,4 @@
1
- import type { CryptoGroup, GroupIdentifier } from './types.js';
1
+ import type { CryptoGroup } from './types.js';
2
2
  /** Immutable definition of the shipped Ristretto255 tally group. */
3
3
  export declare const RISTRETTO_GROUP: CryptoGroup;
4
- /** @internal Returns the immutable built-in Ristretto255 group definition. */
5
- export declare const getGroup: (identifier: GroupIdentifier) => CryptoGroup;
6
- /** @internal Lists the immutable built-in group definitions. */
7
- export declare const listGroups: () => readonly CryptoGroup[];
8
- /** Returns the canonical deterministic secondary generator encoding. */
9
- export declare const deriveH: () => string;
4
+ export declare const assertCanonicalRistrettoGroup: (group: CryptoGroup, label?: string) => void;
@@ -10,15 +10,16 @@ export const RISTRETTO_GROUP = Object.freeze({
10
10
  h: derivePedersenGenerator(),
11
11
  securityEstimate: 128,
12
12
  });
13
- const GROUPS = Object.freeze([RISTRETTO_GROUP]);
14
- /** @internal Returns the immutable built-in Ristretto255 group definition. */
15
- export const getGroup = (identifier) => {
16
- if (identifier !== RISTRETTO_GROUP.name) {
17
- throw new UnsupportedSuiteError(`Unsupported group: ${String(identifier)}`);
13
+ const sameCanonicalRistrettoGroup = (group) => group.name === RISTRETTO_GROUP.name &&
14
+ group.byteLength === RISTRETTO_GROUP.byteLength &&
15
+ group.scalarByteLength === RISTRETTO_GROUP.scalarByteLength &&
16
+ group.q === RISTRETTO_GROUP.q &&
17
+ group.g === RISTRETTO_GROUP.g &&
18
+ group.h === RISTRETTO_GROUP.h &&
19
+ group.securityEstimate === RISTRETTO_GROUP.securityEstimate;
20
+ export const assertCanonicalRistrettoGroup = (group, label = 'Group') => {
21
+ const normalizedLabel = label.length > 0 ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
22
+ if (!sameCanonicalRistrettoGroup(group)) {
23
+ throw new UnsupportedSuiteError(`${normalizedLabel} must match the shipped canonical ristretto255 group definition`);
18
24
  }
19
- return RISTRETTO_GROUP;
20
25
  };
21
- /** @internal Lists the immutable built-in group definitions. */
22
- export const listGroups = () => GROUPS;
23
- /** Returns the canonical deterministic secondary generator encoding. */
24
- export const deriveH = () => RISTRETTO_GROUP.h;
@@ -1,21 +1,17 @@
1
1
  import { ristretto255 } from '@noble/curves/ed25519.js';
2
2
  import type { EncodedPoint } from './types.js';
3
- export type InternalPoint = InstanceType<typeof ristretto255.Point>;
4
- export declare const RISTRETTO_GROUP_NAME = "ristretto255";
3
+ type InternalPoint = InstanceType<typeof ristretto255.Point>;
5
4
  export declare const RISTRETTO_ORDER: bigint;
6
5
  export declare const RISTRETTO_BYTE_LENGTH = 32;
7
- export declare const RISTRETTO_SECURITY_ESTIMATE = 128;
8
- export declare const RISTRETTO_BASE: InternalPoint;
9
6
  export declare const RISTRETTO_ZERO: InternalPoint;
10
7
  export declare const encodePoint: (point: InternalPoint) => EncodedPoint;
11
8
  export declare const decodePoint: (value: string, label?: string) => InternalPoint;
12
9
  export declare const encodeScalar: (value: bigint) => string;
13
10
  export declare const decodeScalar: (value: string, label?: string) => bigint;
14
- export declare const assertValidPoint: (value: string, label?: string) => void;
15
- export declare const assertNonIdentityPoint: (value: string, label?: string) => void;
16
11
  export declare const pointAdd: (left: InternalPoint, right: InternalPoint) => InternalPoint;
17
12
  export declare const pointSubtract: (left: InternalPoint, right: InternalPoint) => InternalPoint;
18
13
  export declare const pointMultiply: (point: InternalPoint, scalar: bigint) => InternalPoint;
19
14
  export declare const multiplyBase: (scalar: bigint) => InternalPoint;
20
15
  export declare const derivePedersenGenerator: () => EncodedPoint;
21
16
  export declare const hashChallengeToScalar: (payload: Uint8Array) => bigint;
17
+ export {};
@@ -3,12 +3,10 @@ import { sha512 } from '@noble/hashes/sha2.js';
3
3
  import { bytesToBigInt, bytesToBigIntLE, bytesToHex, hexToBytes, } from './bytes.js';
4
4
  import { utf8ToBytes } from './crypto.js';
5
5
  import { InvalidGroupElementError, InvalidPayloadError, InvalidScalarError, } from './errors.js';
6
- export const RISTRETTO_GROUP_NAME = 'ristretto255';
7
6
  const RISTRETTO_POINT = ristretto255.Point;
8
7
  export const RISTRETTO_ORDER = ristretto255.Point.Fn.ORDER;
9
8
  export const RISTRETTO_BYTE_LENGTH = 32;
10
- export const RISTRETTO_SECURITY_ESTIMATE = 128;
11
- export const RISTRETTO_BASE = ristretto255.Point.BASE;
9
+ const RISTRETTO_BASE = ristretto255.Point.BASE;
12
10
  export const RISTRETTO_ZERO = ristretto255.Point.ZERO;
13
11
  const scalarByteLength = RISTRETTO_BYTE_LENGTH;
14
12
  const pointHexLength = RISTRETTO_BYTE_LENGTH * 2;
@@ -48,14 +46,6 @@ export const decodeScalar = (value, label = 'Scalar encoding') => {
48
46
  }
49
47
  return scalar;
50
48
  };
51
- export const assertValidPoint = (value, label = 'Point') => {
52
- decodePoint(value, label);
53
- };
54
- export const assertNonIdentityPoint = (value, label = 'Point') => {
55
- if (decodePoint(value, label).is0()) {
56
- throw new InvalidGroupElementError(`${label} must not be the identity`);
57
- }
58
- };
59
49
  export const pointAdd = (left, right) => left.add(right);
60
50
  export const pointSubtract = (left, right) => left.subtract(right);
61
51
  export const pointMultiply = (point, scalar) => {
@@ -11,8 +11,6 @@ export type GroupName = 'ristretto255';
11
11
  export type GroupIdentifier = GroupName;
12
12
  /** Canonical 32-byte Ristretto point encoding exposed at the public boundary. */
13
13
  export type EncodedPoint = Brand<string, 'EncodedPoint'>;
14
- /** Scalar value intended to live in the prime-order field `Z_q`. */
15
- export type ScalarQ = Brand<bigint, 'ScalarQ'>;
16
14
  /** @internal Immutable built-in group definition for the shipped suite. */
17
15
  export type CryptoGroup = {
18
16
  /** Canonical suite name. */
@@ -1,4 +1,5 @@
1
1
  import type { PhaseCheckpointPayload, SignedPayload } from '../protocol/types.js';
2
+ import { type ResolvePhaseCheckpointInput } from './verification.js';
2
3
  /** Finalized threshold-supported checkpoint for one DKG phase. */
3
4
  export type FinalizedPhaseCheckpoint = {
4
5
  readonly payload: PhaseCheckpointPayload;
@@ -7,14 +8,12 @@ export type FinalizedPhaseCheckpoint = {
7
8
  };
8
9
  /** Returns `true` when the signed payload is a phase checkpoint. */
9
10
  export declare const isPhaseCheckpointPayload: (payload: SignedPayload) => payload is SignedPayload<PhaseCheckpointPayload>;
10
- /** Returns the required checkpoint phases for the shipped GJKR reducer. */
11
- export declare const requiredCheckpointPhases: () => readonly number[];
12
- /** Returns the last required checkpoint phase for the shipped GJKR reducer. */
13
- export declare const finalCheckpointPhase: () => number;
14
- /** Groups all checkpoint variants observed for one closed DKG phase. */
15
- export declare const collectCheckpointVariants: (transcript: readonly SignedPayload[], checkpointPhase: number) => readonly FinalizedPhaseCheckpoint[];
16
11
  /**
17
- * Returns the unique threshold-supported checkpoint variant for one phase, or
18
- * `null` when no unique threshold-supported checkpoint exists yet.
12
+ * Rejects phase-checkpoint payloads outside the shipped GJKR checkpoint plan.
19
13
  */
20
- export declare const resolveFinalizedPhaseCheckpoint: (transcript: readonly SignedPayload[], checkpointPhase: number, threshold: number) => FinalizedPhaseCheckpoint | null;
14
+ export declare const assertSupportedCheckpointPayloads: (transcript: readonly SignedPayload[]) => void;
15
+ /**
16
+ * Resolves and validates the unique threshold-supported checkpoint for one
17
+ * closed DKG phase.
18
+ */
19
+ export declare const resolveVerifiedPhaseCheckpoint: (input: ResolvePhaseCheckpointInput) => Promise<FinalizedPhaseCheckpoint>;
@@ -1,3 +1,6 @@
1
+ import { InvalidPayloadError } from '../core/index.js';
2
+ import { hashProtocolPhaseSnapshot } from '../protocol/transcript.js';
3
+ import { assertIndexSubset, assertUniqueSortedParticipantIndices, } from './verification.js';
1
4
  const checkpointKey = (payload) => JSON.stringify({
2
5
  sessionId: payload.sessionId,
3
6
  manifestHash: payload.manifestHash,
@@ -5,17 +8,15 @@ const checkpointKey = (payload) => JSON.stringify({
5
8
  messageType: payload.messageType,
6
9
  checkpointPhase: payload.checkpointPhase,
7
10
  checkpointTranscriptHash: payload.checkpointTranscriptHash,
8
- qualParticipantIndices: payload.qualParticipantIndices,
11
+ qualifiedParticipantIndices: payload.qualifiedParticipantIndices,
9
12
  });
10
13
  const compareNumbers = (left, right) => left - right;
14
+ const sameParticipantIndexList = (left, right) => left.length === right.length &&
15
+ left.every((participantIndex, index) => participantIndex === right[index]);
11
16
  /** Returns `true` when the signed payload is a phase checkpoint. */
12
17
  export const isPhaseCheckpointPayload = (payload) => payload.payload.messageType === 'phase-checkpoint';
13
- /** Returns the required checkpoint phases for the shipped GJKR reducer. */
14
- export const requiredCheckpointPhases = () => [0, 1, 2, 3];
15
- /** Returns the last required checkpoint phase for the shipped GJKR reducer. */
16
- export const finalCheckpointPhase = () => requiredCheckpointPhases()[requiredCheckpointPhases().length - 1] ?? 0;
17
- /** Groups all checkpoint variants observed for one closed DKG phase. */
18
- export const collectCheckpointVariants = (transcript, checkpointPhase) => {
18
+ const requiredCheckpointPhases = () => [0, 1, 2, 3];
19
+ const collectCheckpointVariants = (transcript, checkpointPhase) => {
19
20
  const grouped = new Map();
20
21
  for (const signedPayload of transcript) {
21
22
  if (!isPhaseCheckpointPayload(signedPayload) ||
@@ -40,10 +41,47 @@ export const collectCheckpointVariants = (transcript, checkpointPhase) => {
40
41
  });
41
42
  };
42
43
  /**
43
- * Returns the unique threshold-supported checkpoint variant for one phase, or
44
- * `null` when no unique threshold-supported checkpoint exists yet.
44
+ * Rejects phase-checkpoint payloads outside the shipped GJKR checkpoint plan.
45
45
  */
46
- export const resolveFinalizedPhaseCheckpoint = (transcript, checkpointPhase, threshold) => {
47
- const supported = collectCheckpointVariants(transcript, checkpointPhase).filter((entry) => entry.signatures.length >= threshold);
48
- return supported.length === 1 ? supported[0] : null;
46
+ export const assertSupportedCheckpointPayloads = (transcript) => {
47
+ for (const signedPayload of transcript) {
48
+ if (isPhaseCheckpointPayload(signedPayload) &&
49
+ !requiredCheckpointPhases().includes(signedPayload.payload.checkpointPhase)) {
50
+ throw new InvalidPayloadError(`Checkpoint phase ${signedPayload.payload.checkpointPhase} is not part of the GJKR phase plan`);
51
+ }
52
+ }
53
+ };
54
+ /**
55
+ * Resolves and validates the unique threshold-supported checkpoint for one
56
+ * closed DKG phase.
57
+ */
58
+ export const resolveVerifiedPhaseCheckpoint = async (input) => {
59
+ const supported = collectCheckpointVariants(input.transcript, input.checkpointPhase).filter((entry) => entry.signatures.length >= input.threshold);
60
+ if (supported.length === 0) {
61
+ throw new InvalidPayloadError(`Missing threshold-supported phase checkpoint for phase ${input.checkpointPhase}`);
62
+ }
63
+ if (supported.length > 1) {
64
+ throw new InvalidPayloadError(`Observed multiple threshold-supported phase checkpoints for phase ${input.checkpointPhase}`);
65
+ }
66
+ const checkpoint = supported[0];
67
+ const qualifiedParticipantIndices = checkpoint.payload.qualifiedParticipantIndices;
68
+ assertUniqueSortedParticipantIndices(qualifiedParticipantIndices, input.participantCount, `Phase ${input.checkpointPhase} checkpoint qualified participant`);
69
+ if (!sameParticipantIndexList(qualifiedParticipantIndices, input.expectedQualifiedParticipantIndices)) {
70
+ throw new InvalidPayloadError(`Phase ${input.checkpointPhase} checkpoint qualified participant set does not match the verifier-computed active participant set`);
71
+ }
72
+ if (qualifiedParticipantIndices.length < input.threshold) {
73
+ throw new InvalidPayloadError(`Checkpoint qualified participant set for phase ${input.checkpointPhase} must contain at least ${input.threshold} participants`);
74
+ }
75
+ const expectedSnapshotHash = await hashProtocolPhaseSnapshot(input.transcript.map((entry) => entry.payload), input.checkpointPhase);
76
+ if (checkpoint.payload.checkpointTranscriptHash !== expectedSnapshotHash) {
77
+ throw new InvalidPayloadError(`Phase ${input.checkpointPhase} checkpoint transcript hash does not match the signed transcript snapshot`);
78
+ }
79
+ assertIndexSubset(checkpoint.signers, input.signerUniverse, `Phase ${input.checkpointPhase} checkpoint signer`);
80
+ const qualifiedParticipantSet = new Set(qualifiedParticipantIndices);
81
+ for (const signer of checkpoint.signers) {
82
+ if (!qualifiedParticipantSet.has(signer)) {
83
+ throw new InvalidPayloadError(`Phase ${input.checkpointPhase} checkpoint signer ${signer} is not part of the checkpoint qualified participant set`);
84
+ }
85
+ }
86
+ return checkpoint;
49
87
  };
@@ -1,6 +1,53 @@
1
- import { InvalidPayloadError } from '../core/index.js';
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/index.js';
4
+ const pedersenShareEnvelopeKeys = [
5
+ 'blindingValue',
6
+ 'index',
7
+ 'secretValue',
8
+ ];
9
+ const fixedLowercaseHexPattern = /^[0-9a-f]+$/;
10
+ const pedersenShareTextDecoder = new TextDecoder('utf-8', { fatal: true });
11
+ const assertCanonicalEnvelopeIndex = (value, expectedParticipantIndex, label) => {
12
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
13
+ throw new InvalidPayloadError(`${label} share index must be a positive integer`);
14
+ }
15
+ if (value !== expectedParticipantIndex) {
16
+ throw new InvalidPayloadError(`${label} share index mismatch: expected ${expectedParticipantIndex}, received ${value}`);
17
+ }
18
+ return value;
19
+ };
20
+ const parseCanonicalFixedHex = (value, byteLength, label) => {
21
+ const expectedLength = byteLength * 2;
22
+ if (typeof value !== 'string' ||
23
+ value.length !== expectedLength ||
24
+ !fixedLowercaseHexPattern.test(value)) {
25
+ throw new InvalidPayloadError(`${label} must be a lowercase fixed-width hexadecimal string of length ${expectedLength}`);
26
+ }
27
+ const decoded = fixedHexToBigInt(value);
28
+ if (bigintToFixedHex(decoded, byteLength) !== value) {
29
+ throw new InvalidPayloadError(`${label} must use canonical fixed-width hexadecimal encoding`);
30
+ }
31
+ return decoded;
32
+ };
33
+ const parsePedersenShareEnvelopeRecord = (parsed, expectedParticipantIndex, label) => {
34
+ if (parsed === null ||
35
+ Array.isArray(parsed) ||
36
+ typeof parsed !== 'object') {
37
+ throw new InvalidPayloadError(`${label} plaintext must be a JSON object`);
38
+ }
39
+ const record = parsed;
40
+ const keys = Object.keys(record).sort();
41
+ if (keys.length !== pedersenShareEnvelopeKeys.length ||
42
+ !keys.every((key, index) => key === pedersenShareEnvelopeKeys[index])) {
43
+ throw new InvalidPayloadError(`${label} plaintext must contain only blindingValue, index, and secretValue`);
44
+ }
45
+ return {
46
+ blindingValue: bigintToFixedHex(parseCanonicalFixedHex(record.blindingValue, RISTRETTO_GROUP.scalarByteLength, `${label} blinding value`), RISTRETTO_GROUP.scalarByteLength),
47
+ index: assertCanonicalEnvelopeIndex(record.index, expectedParticipantIndex, label),
48
+ secretValue: bigintToFixedHex(parseCanonicalFixedHex(record.secretValue, RISTRETTO_GROUP.scalarByteLength, `${label} secret value`), RISTRETTO_GROUP.scalarByteLength),
49
+ };
50
+ };
4
51
  /** Encodes one Pedersen share pair for encrypted dealer-to-recipient transport. */
5
52
  export const encodePedersenShareEnvelope = (share, byteLength) => canonicalizeJson({
6
53
  index: share.index,
@@ -11,19 +58,29 @@ export const encodePedersenShareEnvelope = (share, byteLength) => canonicalizeJs
11
58
  });
12
59
  /** Decodes one encrypted Pedersen share pair after envelope decryption. */
13
60
  export const decodePedersenShareEnvelope = (plaintext, expectedParticipantIndex, label) => {
14
- let parsed;
61
+ let decodedPlaintext;
15
62
  try {
16
- parsed = JSON.parse(new TextDecoder().decode(plaintext));
63
+ decodedPlaintext = pedersenShareTextDecoder.decode(plaintext);
17
64
  }
18
65
  catch {
19
66
  throw new InvalidPayloadError(`${label} plaintext is not valid canonical JSON`);
20
67
  }
21
- if (parsed.index !== expectedParticipantIndex) {
22
- throw new InvalidPayloadError(`${label} share index mismatch: expected ${expectedParticipantIndex}, received ${parsed.index}`);
68
+ let parsed;
69
+ try {
70
+ parsed = parsePedersenShareEnvelopeRecord(JSON.parse(decodedPlaintext), expectedParticipantIndex, label);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof InvalidPayloadError) {
74
+ throw error;
75
+ }
76
+ throw new InvalidPayloadError(`${label} plaintext is not valid canonical JSON`);
77
+ }
78
+ if (canonicalizeJson(parsed) !== decodedPlaintext) {
79
+ throw new InvalidPayloadError(`${label} plaintext must use canonical JSON encoding`);
23
80
  }
24
81
  return {
25
82
  index: parsed.index,
26
- secretValue: fixedHexToBigint(parsed.secretValue),
27
- blindingValue: fixedHexToBigint(parsed.blindingValue),
83
+ secretValue: fixedHexToBigInt(parsed.secretValue),
84
+ blindingValue: fixedHexToBigInt(parsed.blindingValue),
28
85
  };
29
86
  };
@@ -2,9 +2,9 @@ import { type CryptoGroup } from '../core/index.js';
2
2
  import type { EncodedPoint } from '../core/types.js';
3
3
  import type { ComplaintPayload, SignedPayload } from '../protocol/types.js';
4
4
  import type { VerifiedProtocolSignatures } from '../protocol/verification.js';
5
- import type { EncryptedShareMatrix, VerifyDKGTranscriptInput } from './verification-types.js';
5
+ import type { EncryptedShareMatrix, VerifyDKGTranscriptInput } from './verification.js';
6
6
  export declare const buildEncryptedShareMatrix: (transcript: readonly SignedPayload[], participantCount: number) => EncryptedShareMatrix;
7
7
  export declare const assertEncryptedShareCoverage: (encryptedShareMatrix: EncryptedShareMatrix, participantIndices: readonly number[]) => void;
8
- export declare const parsePedersenCommitmentMap: (transcript: readonly SignedPayload[], threshold: number, group: CryptoGroup) => ReadonlyMap<number, readonly EncodedPoint[]>;
8
+ export declare const parsePedersenCommitmentMap: (transcript: readonly SignedPayload[], threshold: number) => ReadonlyMap<number, readonly EncodedPoint[]>;
9
9
  export declare const assertPedersenCommitmentCoverage: (pedersenCommitmentMap: ReadonlyMap<number, readonly EncodedPoint[]>, dealerIndices: readonly number[]) => void;
10
10
  export declare const verifyComplaintOutcomes: (input: VerifyDKGTranscriptInput, verifiedSignatures: VerifiedProtocolSignatures, encryptedShareMatrix: EncryptedShareMatrix, pedersenCommitmentMap: ReadonlyMap<number, readonly EncodedPoint[]>, group: CryptoGroup, allowedParticipants: ReadonlySet<number>) => Promise<readonly ComplaintPayload[]>;