Skip to content

Commit

Permalink
TECH-2505 New receipt format (#297)
Browse files Browse the repository at this point in the history
* extra receipt parameters

* tests for vote receipt

* revert to BoardItem

* commented out CastRequest Item

* another try with CastRequestItem type

* remove unused code

* receipt is a base64 string

* base64 hack

* some commit at the end of the day

* use existing functionality

* isReceiptValid working + specs

* fixed some indentation

* introduced validation errors

* Fixed spec values

* convert only specific error to specialized errors

* bugfix

* fix grammar

* fixed some indentation

* more specific spec

* alexis fix

* bump version

* bump version

---------

Co-authored-by: av-alexistoledo <alexis.toledo@assemblyvoting.com>
  • Loading branch information
stefan-av and av-alexistoledo authored Aug 6, 2024
1 parent f73e5cc commit 0975e09
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 23 deletions.
9 changes: 9 additions & 0 deletions lib/av_client/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ export class InvalidTrackingCodeError extends AvClientError {
}
}

export class InvalidReceiptError extends AvClientError {
readonly name = "InvalidReceiptError";

constructor(message: string) {
super(message);
Object.setPrototypeOf(this, InvalidReceiptError.prototype);
}
}

export class InvalidContestError extends AvClientError {
readonly name = "InvalidContestError";

Expand Down
18 changes: 11 additions & 7 deletions lib/av_client/generate_receipt.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { hexToShortCode } from "./short_codes";
import { BallotBoxReceipt, BoardItem } from "./types"
import { BallotBoxReceipt, CastRequestItem } from "./types"

export function generateReceipt(serverReceipt: string, castRequest: BoardItem): BallotBoxReceipt {
export function generateReceipt(serverReceipt: string, castRequest: CastRequestItem): BallotBoxReceipt {
const receiptData = {
address: castRequest.address,
parentAddress: castRequest.parentAddress,
previousAddress: castRequest.previousAddress,
registeredAt: castRequest.registeredAt,
dbbSignature: serverReceipt,
voterSignature: castRequest.signature
}
return {
trackingCode: hexToShortCode(castRequest.address.substring(0,10)),
receipt: {
address: castRequest.address,
dbbSignature: serverReceipt,
voterSignature: castRequest.signature
}
receipt: btoa(JSON.stringify(receiptData))
}
}
2 changes: 1 addition & 1 deletion lib/av_client/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const verifyContent = (actual: Record<string, unknown>, expectations: Record<str
}
};

const verifyAddress = (item: BoardItem) => {
export const verifyAddress = (item: BoardItem) => {
const uniformer = new Uniformer();

const addressHashSource = uniformer.formString({
Expand Down
14 changes: 8 additions & 6 deletions lib/av_client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,15 @@ export interface Election {
*/
export type BallotBoxReceipt = {
trackingCode: string
receipt: {
address: string
dbbSignature: string
voterSignature: string
}
receipt: string
}

export type BoardItem =
VoterSessionItem |
VoterCommitmentItem |
BoardCommitmentItem |
BallotCryptogramItem
BallotCryptogramItem |
CastRequestItem

export type BoardItemType =
"BallotCryptogramsItem" |
Expand Down Expand Up @@ -187,6 +184,11 @@ export interface SpoilRequestItem extends BaseBoardItem {
type: "SpoilRequestItem"
}

export interface CastRequestItem extends BaseBoardItem {
content: Record<string, never> // empty object
type: "CastRequestItem"
}

export interface CommitmentOpening {
randomizers: ContestMap<string[][]>
commitmentRandomness: string
Expand Down
63 changes: 55 additions & 8 deletions lib/av_verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import {
VERIFIER_ITEM
} from './av_client/constants';
import {randomKeyPair} from './av_client/generate_key_pair';
import {signPayload} from './av_client/sign';
import {signPayload, validateReceipt, verifyAddress} from './av_client/sign';
import {
VerifierItem,
BoardCommitmentOpeningItem,
VoterCommitmentOpeningItem,
BallotCryptogramItem,
ContestSelection,
ReadableContestSelection,
LatestConfig
LatestConfig, CastRequestItem
} from './av_client/types';
import {hexToShortCode, shortCodeToHex} from './av_client/short_codes';
import {fetchLatestConfig} from './av_client/election_config';
import {decryptCommitmentOpening, validateCommmitmentOpening} from './av_client/crypto/commitments';
import {InvalidContestError, InvalidTrackingCodeError} from './av_client/errors';
import {InvalidContestError, InvalidReceiptError, InvalidTrackingCodeError} from './av_client/errors';
import {decryptContestSelections} from './av_client/decrypt_contest_selections';
import {makeOptionFinder} from './av_client/option_finder';

Expand Down Expand Up @@ -173,13 +173,9 @@ export class AVVerifier {
piles: readablePiles
}
})


}

public async

pollForCommitmentOpening() {
public async pollForCommitmentOpening() {
let attempts = 0;

const executePoll = async (resolve, reject) => {
Expand All @@ -202,6 +198,57 @@ export class AVVerifier {

return new Promise(executePoll);
}

public validateReceipt(encodedReceipt: string, trackingCode: string) {
const [castRequestItem, receipt] = this.parseReceipt(encodedReceipt)
this.validateTrackingCode(trackingCode, castRequestItem)

try {
verifyAddress(castRequestItem)
validateReceipt([castRequestItem], receipt, this.latestConfig.items.genesisConfig.content.publicKey)
} catch (err) {
// This checks for the specific error messages that invalidate a receipt. Other different errors would bubble up.
if (
/^Unknown parameter type /.test(err.message) || // if the unifier encounters unsupported data types
/^BoardItem address does not match expected address /.test(err.message) || // if crypto fails on validating the address
err.message == "Board receipt verification failed" // if crypto fails on validating the dbb signature
) {
throw new InvalidReceiptError(err.message)
} else {
throw err
}
}
}

private validateTrackingCode(trackingCode: string, castRequestItem: CastRequestItem) {
const shortAddress = shortCodeToHex(trackingCode)
if (shortAddress != castRequestItem.address.substring(0,10)) {
throw new InvalidTrackingCodeError("Tracking code does not match the receipt")
}
}

private parseReceipt(encodedReceipt: string) {
let receiptData
try {
receiptData = JSON.parse(atob(encodedReceipt))
} catch (err) {
throw new InvalidReceiptError("Receipt string is invalid")
}

const castRequestItem: CastRequestItem = {
type: "CastRequestItem",
author: "",
address: receiptData.address,
parentAddress: receiptData.parentAddress,
previousAddress: receiptData.previousAddress,
content: {},
registeredAt: receiptData.registeredAt,
signature: receiptData.voterSignature
}
const receipt = receiptData.dbbSignature

return [castRequestItem, receipt]
}
}

function makeLocalizer(locale: string) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "4.1.2",
"version": "4.2.0",
"name": "@aion-dk/js-client",
"license": "MIT",
"description": "Assembly Voting JS client",
Expand Down
23 changes: 23 additions & 0 deletions test/generate_receipt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {expect} from 'chai';
import {generateReceipt} from '../lib/av_client/generate_receipt';
import {CastRequestItem} from '../lib/av_client/types';
import {baseItemAttributes} from "./fixtures/itemHelper";

const castRequest: CastRequestItem = {
...baseItemAttributes(),
content: {},
type: 'CastRequestItem'
}

const serverReceipt = "dummy signature string"

describe('generateReceipt', () => {
context('when given a valid arguments', () => {
it('constructs a vote receipt', async () => {
const voteReceipt = generateReceipt(serverReceipt, castRequest)
expect(voteReceipt).to.have.keys('trackingCode', 'receipt')
expect(voteReceipt.trackingCode).to.be.a("string")
expect(voteReceipt.receipt).to.be.a("string")
})
})
})
105 changes: 105 additions & 0 deletions test/verifier/receipt_verification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { AVVerifier } from '../../lib/av_verifier';
import { expect } from 'chai';
import sinon = require('sinon');
import { bulletinBoardHost } from '../test_helpers'
import { LatestConfig } from '../../lib/av_client/types';
import latestConfig from '../fixtures/latestConfig';
import {InvalidReceiptError, InvalidTrackingCodeError} from "../../lib/av_client/errors";
import * as Crypto from '../../lib/av_client/aion_crypto';


describe('#isReceiptValid', () => {
let verifier: AVVerifier;
const config: LatestConfig = latestConfig;

beforeEach(async () => {
verifier = new AVVerifier(bulletinBoardHost + 'us');
await verifier.initialize(config)
});

context('given valid receipt', () => {
const receipt =
"eyJhZGRyZXNzIjoiMDFkOTc2OTZjNTlmYWFmMWFiYjhmNDJhZDY2MTMxZGUwNThkZWE4MTU1N2NiNTI2N2E0ZjcwOTlkMjNhNjEzZiIsInBh\n" +
"cmVudEFkZHJlc3MiOiI5MmVmOTU0MzcyNmEyZDhlMjFiMGVlOGE0ZDQwMDdlZGE1MzkzYzMyMDA2ZjU4ZWFhMTJkZTczNzQ2MjQ3NWU0Iiwi\n" +
"cHJldmlvdXNBZGRyZXNzIjoiOTJlZjk1NDM3MjZhMmQ4ZTIxYjBlZThhNGQ0MDA3ZWRhNTM5M2MzMjAwNmY1OGVhYTEyZGU3Mzc0NjI0NzVl\n" +
"NCIsInJlZ2lzdGVyZWRBdCI6IjIwMjQtMDctMzBUMTE6NDY6MzUuMDc3WiIsImRiYlNpZ25hdHVyZSI6IjYwNzMzNTI4MTYzZTM5ZDk2ZDJl\n" +
"YTUxNWNjZjZlMjA2MTdiZjllOWQyNTcyZmYzZjRlMjU0ODQ2ZjczZjRlNTYsMDk4ZDcxYTdlYTAzYjY2NDUwYTk0ZDIzMWQzNTViNjZmMTNh\n" +
"YzI4NDZhMzhjODk4ZGEzNjRjOGI3MDJhY2YwNyIsInZvdGVyU2lnbmF0dXJlIjoiMWFhYWZiZWNhMjdiYWE1ZWQ4ZDUxMDg2OWIyNzg3ZDk3\n" +
"NWQ4M2M4MjRhYzZmMGRhYWZhMzA2YjVlZDMzZGY3YSwyZDUzN2Q5ZWUzZGE0YWM4YjU1MjM3N2U1YTk2MmY0OGNmNmVmZTNmN2M1MzVkNTc5\n" +
"MDc2Mjg5NGRkYmNlODk2In0="
const trackingCode = "1D6vybS"

before(() => {
config.items.genesisConfig.content.publicKey = "029abf158b2438e561afe4bc5b85629d46610a526c8a6284f24076c4e4b03264aa"
})

it('succeeds', async () => {
expect(() => verifier.validateReceipt(receipt, trackingCode)).not.to.throw
});

context('given a different signing key', () => {
before(() => {
config.items.genesisConfig.content.publicKey = "0220f81d43002c88229ed8c80cfc7f84f9700ee13d80e1be1cd8a3677f84e99ae1"
})

it('throws validation error', async () => {
expect(() => verifier.validateReceipt(receipt, trackingCode)).to.throw(InvalidReceiptError, 'Board receipt verification failed')
});
});

context('given a mismatching tracking code', () => {
const trackingCode = "1nvaLid"

it('throws validation error', async () => {
expect(() => verifier.validateReceipt(receipt, trackingCode)).to.throw(InvalidTrackingCodeError, 'Tracking code does not match the receipt')
});
});
});

context('given invalid receipt', () => {
const receipt = "invalid"
const trackingCode = "1D6vybS"

it('returns throws error', async () => {
expect(() => verifier.validateReceipt(receipt, trackingCode)).to.throw(InvalidReceiptError, "Receipt string is invalid");
});

context('given an item with broken address', () => {
// The registered_at attribute is changed
const receipt =
"eyJhZGRyZXNzIjoiMDFkOTc2OTZjNTlmYWFmMWFiYjhmNDJhZDY2MTMxZGUwNThkZWE4MTU1N2NiNTI2N2E0ZjcwOTlkMjNhNjEzZiIsInBh\n" +
"cmVudEFkZHJlc3MiOiI5MmVmOTU0MzcyNmEyZDhlMjFiMGVlOGE0ZDQwMDdlZGE1MzkzYzMyMDA2ZjU4ZWFhMTJkZTczNzQ2MjQ3NWU0Iiwi\n" +
"cHJldmlvdXNBZGRyZXNzIjoiOTJlZjk1NDM3MjZhMmQ4ZTIxYjBlZThhNGQ0MDA3ZWRhNTM5M2MzMjAwNmY1OGVhYTEyZGU3Mzc0NjI0NzVl\n" +
"NCIsInJlZ2lzdGVyZWRBdCI6IjIwMjMtMDctMzBUMTE6NDY6MzUuMDc3WiIsImRiYlNpZ25hdHVyZSI6IjYwNzMzNTI4MTYzZTM5ZDk2ZDJl\n" +
"YTUxNWNjZjZlMjA2MTdiZjllOWQyNTcyZmYzZjRlMjU0ODQ2ZjczZjRlNTYsMDk4ZDcxYTdlYTAzYjY2NDUwYTk0ZDIzMWQzNTViNjZmMTNh\n" +
"YzI4NDZhMzhjODk4ZGEzNjRjOGI3MDJhY2YwNyIsInZvdGVyU2lnbmF0dXJlIjoiMWFhYWZiZWNhMjdiYWE1ZWQ4ZDUxMDg2OWIyNzg3ZDk3\n" +
"NWQ4M2M4MjRhYzZmMGRhYWZhMzA2YjVlZDMzZGY3YSwyZDUzN2Q5ZWUzZGE0YWM4YjU1MjM3N2U1YTk2MmY0OGNmNmVmZTNmN2M1MzVkNTc5\n" +
"MDc2Mjg5NGRkYmNlODk2In0="

it('returns false', async () => {
expect(() => verifier.validateReceipt(receipt, trackingCode)).to.throw(InvalidReceiptError, 'BoardItem address does not match expected address')
});
});
});

context('when a generic error is thrown', () => {
const receipt =
"eyJhZGRyZXNzIjoiMDFkOTc2OTZjNTlmYWFmMWFiYjhmNDJhZDY2MTMxZGUwNThkZWE4MTU1N2NiNTI2N2E0ZjcwOTlkMjNhNjEzZiIsInBh\n" +
"cmVudEFkZHJlc3MiOiI5MmVmOTU0MzcyNmEyZDhlMjFiMGVlOGE0ZDQwMDdlZGE1MzkzYzMyMDA2ZjU4ZWFhMTJkZTczNzQ2MjQ3NWU0Iiwi\n" +
"cHJldmlvdXNBZGRyZXNzIjoiOTJlZjk1NDM3MjZhMmQ4ZTIxYjBlZThhNGQ0MDA3ZWRhNTM5M2MzMjAwNmY1OGVhYTEyZGU3Mzc0NjI0NzVl\n" +
"NCIsInJlZ2lzdGVyZWRBdCI6IjIwMjQtMDctMzBUMTE6NDY6MzUuMDc3WiIsImRiYlNpZ25hdHVyZSI6IjYwNzMzNTI4MTYzZTM5ZDk2ZDJl\n" +
"YTUxNWNjZjZlMjA2MTdiZjllOWQyNTcyZmYzZjRlMjU0ODQ2ZjczZjRlNTYsMDk4ZDcxYTdlYTAzYjY2NDUwYTk0ZDIzMWQzNTViNjZmMTNh\n" +
"YzI4NDZhMzhjODk4ZGEzNjRjOGI3MDJhY2YwNyIsInZvdGVyU2lnbmF0dXJlIjoiMWFhYWZiZWNhMjdiYWE1ZWQ4ZDUxMDg2OWIyNzg3ZDk3\n" +
"NWQ4M2M4MjRhYzZmMGRhYWZhMzA2YjVlZDMzZGY3YSwyZDUzN2Q5ZWUzZGE0YWM4YjU1MjM3N2U1YTk2MmY0OGNmNmVmZTNmN2M1MzVkNTc5\n" +
"MDc2Mjg5NGRkYmNlODk2In0="
const trackingCode = "1D6vybS"

before(() => {
sinon.replace(Crypto, 'hashString', sinon.fake.throws(new SyntaxError('Error')));
})

it('bubbles up', () => {
expect(() => verifier.validateReceipt(receipt, trackingCode)).to.throw(SyntaxError, "Error");
})
})
});

0 comments on commit 0975e09

Please sign in to comment.