Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TECH-2505 New receipt format #297

Merged
merged 22 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
})
})
});
Loading