Skip to content

Commit

Permalink
[TECH-466] Switch configuration endpoints (#209)
Browse files Browse the repository at this point in the history
* `electionConfig` item is now called `latestConfig`.
* `latestConfig` has now different configuration items: `thresholdConfig`, `voterAuthorizerConfig`, `ballotConfigs`, `contestConfigs`, `votingRoundConfigs`, `electionConfig`, `genesisConfig`, and `latestConfigItem`.
* The different configuration items on the `latestConfig` are now wrapped inside `items`.
* Main elements on each configuration item are now wrapped inside `content`.
* The `services` of the former `electionConfig` are now inside the `voterAuthorizerConfig` item.
* `fetchElectionConfig` method is now called `fetchLatestConfig`.
* `validateElectionConfig` method is now called `validateLatestConfig`.
* `getElectionConfig` method is now called `getLatestConfig`.
* Most type declarations were moved to the `types.ts` file.
* Integration/e2e tests were wiped from the JS client.
* Benaloh flow and walkthrough don’t support a mocked version anymore. They can still be used for development purposes (see readme).
* Unit tests for payload and receipt validation were added. 

Co-authored-by: Lukas Alex <101322972+av-lukas@users.noreply.github.com>
Co-authored-by: Alexis Toledo <alexis.toledo@Alexis-Toledos-MacBook-Pro-16.local>
Co-authored-by: av-alexistoledo <112619860+av-alexistoledo@users.noreply.github.com>
Co-authored-by: av-alexistoledo <alexis.toledo@assemblyvoting.com>
Co-authored-by: Martin Laursen <martin@assemblyvoting.com>
Co-authored-by: Lukas Alex <101322972+av-lukas@users.noreply.github.com>
  • Loading branch information
6 people authored Dec 2, 2022
1 parent d67f0c6 commit 497af6f
Show file tree
Hide file tree
Showing 58 changed files with 855 additions and 2,294 deletions.
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,20 @@ To run tests:
yarn run test
````
To run the Benaloh flow and walkthrough test:
Delete the `.skip` instruction on line 9, in the test/benaloh_flow.test.ts, and on lines 11 and 90, in test/walkthrough.test.ts, and make sure you have Devbox’s services running and seeded.
```
yarn run test
````
To run the tests in watch mode:
```
yarn run tdd
```
To record the responses
In the `js-client` go to `walkthrough.test.ts` and disable mocks by changing `const USE_MOCK = true;` -> `false`.
Uncomment `// return await recordResponses(async function() {` in `returns a receipt` test. Remember the closing brackets at the end of the test.
The recorded responses should be saved the `test/replies/otp_flow`-folder.
The folder can be copy pasted to the `ionic-app`-repo by replacing the folder `tests/e2e/fixtures/otp_flow`
To generate HTML documentation for external usage:
```
Expand Down
89 changes: 36 additions & 53 deletions lib/av_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { OTPProvider, IdentityConfirmationToken } from "./av_client/connectors/o
import { extractContestSelections } from './util/nist_cvr_extractor';
import { AVVerifier } from './av_verifier';
import { constructContestEnvelopes } from './av_client/construct_contest_envelopes';
import { KeyPair, Affidavit, VerifierItem, CommitmentOpening, SpoilRequestItem, ElectionConfig, BallotSelection, ContestEnvelope, BallotConfig, BallotStatus } from './av_client/types';
import { KeyPair, Affidavit, VerifierItem, CommitmentOpening, SpoilRequestItem, LatestConfig, BallotSelection, ContestEnvelope, BallotConfig, BallotStatus } from './av_client/types';
import { randomKeyPair } from './av_client/generate_key_pair';
import { generateReceipt } from './av_client/generate_receipt';
import * as jwt from 'jose';

import {
fetchElectionConfig,
validateElectionConfig
fetchLatestConfig,
validateLatestConfig
} from './av_client/election_config';

import {
Expand Down Expand Up @@ -90,7 +90,7 @@ export class AVClient implements IAVClient {
private dbbPublicKey: string | undefined;

private bulletinBoard: BulletinBoard;
private electionConfig?: ElectionConfig;
private latestConfig?: LatestConfig;
private keyPair: KeyPair;

private clientEnvelopes: ContestEnvelope[];
Expand All @@ -115,20 +115,17 @@ export class AVClient implements IAVClient {
* Initializes the client with an election config.
* If no config is provided, it fetches one from the backend.
*
* @param electionConfig Allows injection of an election configuration for testing purposes
* @param latestConfig Allows injection of an election configuration for testing purposes
* @param keyPair Allows injection of a keypair to support automatic testing
* @returns Returns undefined if succeeded or throws an error
* @throws {@link NetworkError | NetworkError } if any request failed to get a response
*/
async initialize(electionConfig: ElectionConfig | undefined, keyPair: KeyPair): Promise<void>
async initialize(electionConfig: ElectionConfig): Promise<void>
async initialize(): Promise<void>
public async initialize(electionConfig?: ElectionConfig, keyPair?: KeyPair): Promise<void> {
if (electionConfig) {
validateElectionConfig(electionConfig);
this.electionConfig = electionConfig;
public async initialize(latestConfig?: LatestConfig, keyPair?: KeyPair): Promise<void> {
if (latestConfig) {
validateLatestConfig(latestConfig);
this.latestConfig = latestConfig;
} else {
this.electionConfig = await fetchElectionConfig(this.bulletinBoard);
this.latestConfig = await fetchLatestConfig(this.bulletinBoard);
}

if (keyPair) {
Expand All @@ -152,8 +149,8 @@ export class AVClient implements IAVClient {
* @throws {@link NetworkError | NetworkError } if any request failed to get a response
*/
public async requestAccessCode(opaqueVoterId: string, email: string): Promise<void> {
const coordinatorURL = this.getElectionConfig().services.voterAuthorizer.url;
const voterAuthorizerContextUuid = this.getElectionConfig().services.voterAuthorizer.electionContextUuid;
const coordinatorURL = this.getLatestConfig().items.voterAuthorizerConfig.content.voterAuthorizer.url;
const voterAuthorizerContextUuid = this.getLatestConfig().items.voterAuthorizerConfig.content.voterAuthorizer.contextUuid;
const coordinator = new VoterAuthorizationCoordinator(coordinatorURL, voterAuthorizerContextUuid);

return coordinator.createSession(opaqueVoterId, email)
Expand Down Expand Up @@ -188,8 +185,8 @@ export class AVClient implements IAVClient {
if(!this.email)
throw new InvalidStateError('Cannot validate access code. Access code was not requested.');

const otpProviderUrl = this.getElectionConfig().services.otpProvider.url;
const otpProviderElectionContextUuid = this.getElectionConfig().services.otpProvider.electionContextUuid;
const otpProviderUrl = this.getLatestConfig().items.voterAuthorizerConfig.content.identityProvider.url;
const otpProviderElectionContextUuid = this.getLatestConfig().items.voterAuthorizerConfig.content.identityProvider.contextUuid;
const provider = new OTPProvider(otpProviderUrl, otpProviderElectionContextUuid)

this.identityConfirmationToken = await provider.requestOTPAuthorization(code, this.email);
Expand All @@ -204,11 +201,11 @@ export class AVClient implements IAVClient {
* Authorization is done by 'proof-of-identity' or 'proof-of-election-codes'
*/
public async createVoterRegistration(): Promise<void> {
const coordinatorURL = this.getElectionConfig().services.voterAuthorizer.url;
const voterAuthorizerContextUuid = this.getElectionConfig().services.voterAuthorizer.electionContextUuid;
const coordinatorURL = this.getLatestConfig().items.voterAuthorizerConfig.content.voterAuthorizer.url;
const voterAuthorizerContextUuid = this.getLatestConfig().items.voterAuthorizerConfig.content.voterAuthorizer.contextUuid;
const coordinator = new VoterAuthorizationCoordinator(coordinatorURL, voterAuthorizerContextUuid);
const latestConfigAddress = this.getElectionConfig().latestConfigAddress;
const authorizationMode = this.getElectionConfig().services.voterAuthorizer.authorizationMode
const latestConfigAddress = this.getLatestConfig().items.latestConfigItem.address;
const authorizationMode = this.getLatestConfig().items.voterAuthorizerConfig.content.voterAuthorizer.authorizationMode;

let authorizationResponse: AxiosResponse

Expand All @@ -228,7 +225,7 @@ export class AVClient implements IAVClient {

const { authToken } = authorizationResponse.data;

const decoded = jwt.decodeJwt(authToken); // TODO: Verify against dbb pubkey: this.getElectionConfig().services.voterAuthorizer.public_key);
const decoded = jwt.decodeJwt(authToken); // TODO: Verify against dbb pubkey: this.getLatestConfig().services.voterAuthorizer.public_key);

if(decoded === null)
throw new InvalidTokenError('Auth token could not be decoded');
Expand Down Expand Up @@ -260,7 +257,7 @@ export class AVClient implements IAVClient {
* Registers a voter based on the authorization mode of the Voter Authorizer
* @returns undefined or throws an error
*/
public async registerVoter(keys?: KeyPair): Promise<void> {
public async registerVoter(): Promise<void> {
return this.createVoterRegistration();
}

Expand Down Expand Up @@ -318,7 +315,7 @@ export class AVClient implements IAVClient {

const state = {
voterSession: this.voterSession,
electionConfig: this.getElectionConfig(),
latestConfig: this.getLatestConfig(),
};

const {
Expand All @@ -333,7 +330,7 @@ export class AVClient implements IAVClient {
commitmentRandomness: pedersenCommitment.randomizer,
randomizers: envelopeRandomizers
}

const {
// voterCommitment, // TODO: Required when spoiling
boardCommitment,
Expand Down Expand Up @@ -396,9 +393,9 @@ export class AVClient implements IAVClient {

let encryptedAffidavit;

if (affidavit && this.electionConfig && this.electionConfig.castRequestItemAttachmentEncryptionKey) {
if (affidavit && this.latestConfig && this.latestConfig.castRequestItemAttachmentEncryptionKey) {
try {
encryptedAffidavit = dhEncrypt(this.electionConfig.castRequestItemAttachmentEncryptionKey, affidavit).toString()
encryptedAffidavit = dhEncrypt(this.latestConfig.castRequestItemAttachmentEncryptionKey, affidavit).toString()

castRequestItem.content['attachment'] = sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(encryptedAffidavit))
} catch (err) {
Expand All @@ -417,7 +414,7 @@ export class AVClient implements IAVClient {

validatePayload(castRequest, castRequestItem);
validateReceipt([castRequest], receipt, this.getDbbPublicKey());

return generateReceipt(receipt, castRequest);
}

Expand Down Expand Up @@ -491,12 +488,12 @@ export class AVClient implements IAVClient {
return
}

public getElectionConfig(): ElectionConfig {
if(!this.electionConfig){
public getLatestConfig(): LatestConfig {
if(!this.latestConfig){
throw new InvalidStateError('No configuration loaded. Did you call initialize()?')
}

return this.electionConfig
return this.latestConfig
}

public getVoterSession(): VoterSessionItem {
Expand All @@ -509,12 +506,12 @@ export class AVClient implements IAVClient {

public getVoterBallotConfig(): BallotConfig {
const voterSession = this.getVoterSession()
const { ballotConfigs } = this.getElectionConfig()
const { items: { ballotConfigs } } = this.getLatestConfig()
return ballotConfigs[voterSession.content.voterGroup]
}

public getDbbPublicKey(): string {
const dbbPublicKeyFromConfig = this.getElectionConfig().dbbPublicKey;
const dbbPublicKeyFromConfig = this.getLatestConfig().items.genesisConfig.content.publicKey;

if(this.dbbPublicKey) {
return this.dbbPublicKey;
Expand All @@ -525,18 +522,10 @@ export class AVClient implements IAVClient {
}
}

private affidavitConfig(): AffidavitConfig {
return this.getElectionConfig().affidavit
}

private privateKey(): BigNum {
return this.keyPair.privateKey
}

private publicKey(): ECPoint {
return this.keyPair.publicKey
}

/**
* Registers a voter by proof of identity
* Used when the authorization mode of the Voter Authorizer is 'proof-of-identity'
Expand Down Expand Up @@ -564,9 +553,9 @@ export class AVClient implements IAVClient {
throw new InvalidStateError('Cannot challenge ballot, no user session')
}

let attempts = 0;
const executePoll = async (resolve, reject) => {
let attempts = 0;

const executePoll = async (resolve, reject) => {
const result = await this.bulletinBoard.getVerifierItem(this.spoilRequest.address).catch(error => {
console.error(error.response.data.error_message)
});
Expand All @@ -582,8 +571,8 @@ export class AVClient implements IAVClient {
setTimeout(executePoll, POLLING_INTERVAL_MS, resolve, reject);
}
};
return new Promise(executePoll);

return new Promise(executePoll);
}

/**
Expand All @@ -606,12 +595,6 @@ export class AVClient implements IAVClient {
}

type BigNum = string;
type ECPoint = string;

type AffidavitConfig = {
curve: string;
encryptionKey: string;
}

export type {
IAVClient,
Expand All @@ -621,7 +604,7 @@ export type {
BallotBoxReceipt,
HashValue,
Signature,
ElectionConfig
LatestConfig
}

export {
Expand Down
4 changes: 2 additions & 2 deletions lib/av_client/connectors/bulletin_board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export class BulletinBoard {
this.voterSessionUuid = voterSessionUuid
}

getElectionConfig(): Promise<AxiosResponse> {
return this.backend.get('configuration');
getLatestConfig(): Promise<AxiosResponse> {
return this.backend.get('configuration/latest_config');
}

// Voting
Expand Down
21 changes: 3 additions & 18 deletions lib/av_client/construct_contest_envelopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { validateBallotSelection } from './validate_selections'
import { InvalidStateError } from './errors'
import { generatePedersenCommitment } from './crypto/pedersen_commitment'
import { encryptContestSelections } from './encrypt_contest_selections'
import { BallotConfigMap, BallotSelection, ContestConfigMap, ContestEnvelope, ContestMap } from './types'
import { BallotSelection, ContestEnvelope, ContestMap, ClientState } from './types'

export function constructContestEnvelopes( state: ClientState, ballotSelection: BallotSelection ): ConstructResult {
const { contestConfigs, ballotConfig, encryptionKey } = extractConfig(state)
Expand All @@ -20,22 +20,6 @@ export function constructContestEnvelopes( state: ClientState, ballotSelection:
}
}

// We define the client state to only require a subset of the electionConfig and voterSession
// This enables us to do less setup in testing.
// If any of the objects passed does not contain the required properties, then the build step will fail.
interface ClientState {
electionConfig: {
encryptionKey: string
ballotConfigs: BallotConfigMap
contestConfigs: ContestConfigMap
}
voterSession: {
content: {
voterGroup: string
}
}
}

type ConstructResult = {
pedersenCommitment: {
commitment: string,
Expand All @@ -52,7 +36,8 @@ function contestEnvelopesRandomizers( contestEnvelopes: ContestEnvelope[] ){

function extractConfig( state: ClientState ){
const { voterGroup } = state.voterSession.content
const { contestConfigs, ballotConfigs, encryptionKey } = state.electionConfig
const { contestConfigs, ballotConfigs } = state.latestConfig.items
const { encryptionKey } = state.latestConfig.items.thresholdConfig.content

const ballotConfig = ballotConfigs[voterGroup]

Expand Down
2 changes: 1 addition & 1 deletion lib/av_client/decrypt_contest_selections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function decryptContestSelections(
const randomizers = combineRandomizers(contestReference, boardCommitmentOpening, voterCommitmentOpening)

const points = decryptPoints(contestCryptograms, randomizers, encryptionKey)
const maxSize = contestConfig.markingType.encoding.maxSize
const maxSize = contestConfig.content.markingType.encoding.maxSize
const encodedContestSelection = pointsToBytes(points, maxSize)
return byteArrayToContestSelection(contestConfig, encodedContestSelection)
})
Expand Down
Loading

0 comments on commit 497af6f

Please sign in to comment.