Skip to content

Commit

Permalink
Merge pull request #200 from aion-dk/qz-484-proof-of-election-codes
Browse files Browse the repository at this point in the history
[QZ-484] Change proof of private key to proof of election codes
  • Loading branch information
av-martin authored Sep 8, 2022
2 parents 9d56a2a + 9ea8b1b commit 1d11d39
Show file tree
Hide file tree
Showing 17 changed files with 422 additions and 39 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
DBB_URL="http://dbb:3003/"
VOTER_AUTHORIZER_URL="http://voter-authorizer:3002/"
CONFERENCE_HOST_URL="http://localhost:3005/"
OTP_PROVIDER_URL="http://otp-provider:3001/"
MAILCATCHER_URL="http://mailcatcher:1080/"
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# AV Client Library Changelog
## 1.1.6
- Replace proof-of-private-key with proof-of-election-codes

## 1.1.5
- Move authorizationMode under voterAuthorizer and rename values:
- proof-of-identity and proof-of-private-key
Expand Down
30 changes: 12 additions & 18 deletions lib/av_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ import { CAST_REQUEST_ITEM, MAX_POLL_ATTEMPTS, POLLING_INTERVAL_MS, SPOIL_REQUES
import { hexToShortCode, shortCodeToHex } from './av_client/short_codes';
import { encryptCommitmentOpening, validateCommmitmentOpening } from './av_client/crypto/commitments';
import { submitBallotCryptograms } from './av_client/actions/submit_ballot_cryptograms';
import { Curve, DiscreteLogarithmProof } from './av_client/aion_crypto'
import {AxiosResponse} from "axios";
import { ProofOfElectionCodes } from "./av_client/crypto/proof_of_election_codes";

/** @internal */
export const sjcl = sjclLib;
Expand Down Expand Up @@ -99,6 +99,7 @@ export class AVClient implements IAVClient {
private ballotCryptogramItem: BallotCryptogramItem;
private voterCommitmentOpening: CommitmentOpening;
private spoilRequest: SpoilRequestItem
private proofOfElectionCodes: ProofOfElectionCodes;

/**
* @param bulletinBoardURL URL to the Assembly Voting backend server, specific for election.
Expand Down Expand Up @@ -192,9 +193,13 @@ export class AVClient implements IAVClient {
this.identityConfirmationToken = await provider.requestOTPAuthorization(code, this.email);
}

generateProofOfElectionCodes(electionCodes : Array<string>) {
this.proofOfElectionCodes = new ProofOfElectionCodes(electionCodes);
}

/**
* Registers a voter based on the authorization mode of the Voter Authorizer
* Authorization is done by 'proof-of-identity' or 'proof-of-private-key'
* Authorization is done by 'proof-of-identity' or 'proof-of-election-codes'
*/
public async createVoterRegistration(): Promise<void> {
const coordinatorURL = this.getElectionConfig().services.voterAuthorizer.url;
Expand All @@ -210,8 +215,11 @@ export class AVClient implements IAVClient {
throw new InvalidStateError('Cannot register voter without identity confirmation. User has not validated access code.')

authorizationResponse = await this.authorizeIdentity(coordinator)
} else if(authorizationMode === 'proof-of-private-key') {
authorizationResponse = await this.authorizePrivateKeyProof(coordinator)
} else if(authorizationMode === 'proof-of-election-codes') {
if(this.proofOfElectionCodes == null)
throw new InvalidStateError('Cannot register voter without proof of election codes. User has not generated an election codes proof.')

authorizationResponse = await coordinator.authorizeProofOfElectionCodes(this.keyPair.publicKey, this.proofOfElectionCodes)
} else {
throw new InvalidConfigError('Unknown authorization mode of voter authorizer')
}
Expand Down Expand Up @@ -525,20 +533,6 @@ export class AVClient implements IAVClient {
);
}

/**
* Registers a voter by proof of private key
* Used when the authorization mode of the Voter Authorizer is 'proof-of-private-key'
* @returns AxiosResponse or throws an error
*/
private async authorizePrivateKeyProof(coordinator): Promise<AxiosResponse> {
const privateKeyBn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(this.keyPair.privateKey))
const proofOfPrivateKey = DiscreteLogarithmProof.generate(Curve.G, privateKeyBn).toString()
return await coordinator.authorizeProof(
this.keyPair.publicKey,
proofOfPrivateKey
);
}

/**
* Should be called after spoilBallot.
* Gets the verifier public key from the DBB
Expand Down
14 changes: 14 additions & 0 deletions lib/av_client/aion_crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,19 @@ function addBigNums(bn1_hex, bn2_hex) {
return sum_hex
}

/**
* Converts an election code string into a big num, for use as a private key.
*
* The private key is derived using pbkdf2-hmac-sha256.
* @param {string} electionCode the code to convert to a private key
* @param {number} curveSize the size of the curve in bits, used to determine the size of the private key
* @returns {string} the hex representation of the private key
*/
function electionCodeToPrivateKey(electionCode, curveSize = 256) {
let privateKeyBits = sjcl.misc.pbkdf2(electionCode, "", 10_000, curveSize);
return sjcl.codec.hex.fromBits(privateKeyBits);
}

module.exports = {
Curve,
DiscreteLogarithmProof,
Expand Down Expand Up @@ -888,4 +901,5 @@ module.exports = {
//'orderObjectByKeys': orderObjectByKeys, // Not used?
hashString,
addBigNums,
electionCodeToPrivateKey
}
8 changes: 5 additions & 3 deletions lib/av_client/connectors/voter_authorization_coordinator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios'
import { IdentityConfirmationToken } from "./otp_provider";
import { EmailDoesNotMatchVoterRecordError, NetworkError, UnsupportedServerReplyError, VoterRecordNotFoundError } from "../errors";
import { ProofOfElectionCodes } from "../crypto/proof_of_election_codes";

export default class VoterAuthorizationCoordinator {
private backend: AxiosInstance;
Expand Down Expand Up @@ -55,11 +56,12 @@ export default class VoterAuthorizationCoordinator {
})
}

authorizeProof(publicKey: string, proof: string): Promise<AxiosResponse> {
authorizeProofOfElectionCodes(publicKey: string, proof: ProofOfElectionCodes): Promise<AxiosResponse> {
return this.backend.post('authorize_proof', {
electionContextUuid: this.electionContextUuid,
publicKey: publicKey,
proof: proof
voterPublicKey: proof.mainKeyPair.publicKey, // This public key is used by the VA to find the voter to authorize.
sessionPublicKey: publicKey, // This public key is used for the auth token
proof: proof.proof,
})
}

Expand Down
21 changes: 21 additions & 0 deletions lib/av_client/crypto/proof_of_election_codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
electionCodeToPrivateKey,
addBigNums,
generateDiscreteLogarithmProof,
generateKeyPair,
} from "../aion_crypto.js";
import {KeyPair} from "../types";

export class ProofOfElectionCodes {
readonly proof: string;
readonly mainKeyPair: KeyPair;

constructor(electionCodes : Array<string>) {
const privateKey = <string>electionCodes
.map(electionCode => electionCodeToPrivateKey(electionCode))
.reduce(addBigNums);
this.proof = generateDiscreteLogarithmProof(privateKey);
const { public_key: publicKey } = generateKeyPair(privateKey);
this.mainKeyPair = { privateKey, publicKey }
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aion-dk/js-client",
"version": "1.1.5",
"version": "1.1.6",
"license": "MIT",
"description": "Assembly Voting JS client",
"main": "dist/lib/av_client.js",
Expand Down
35 changes: 35 additions & 0 deletions test/crypto/aion_crypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect } from "chai";

import { electionCodeToPrivateKey } from "../../lib/av_client/aion_crypto.js"

describe("#electionCodeToPrivateKey", () => {

it("deterministically converts election codes to private keys", () => {
const electionCodes = ['s3cr3t5', 'hidden', '1', 'sellus', 's3cr3t5'];

const privateKeys = electionCodes.map(electionCode => electionCodeToPrivateKey(electionCode));

expect(privateKeys).to.eql([
'631a1838f1e82b7b39f2b620a790de69ca8feb0cfd4ba984350a5fe3a2fda299',
'502b2a265be9cb1d3b25028904ca63687b5384bf862ad1c5ae8929f31e2afd39',
'a259f4b44e30abc0cd53379381bdc86f44723911a5bc03bf4ff21d1b49b53efd',
'1c4e46d624d7de41249bd8d3e5538f3464d43c23e22251468a356c29926b1c24',
'631a1838f1e82b7b39f2b620a790de69ca8feb0cfd4ba984350a5fe3a2fda299',
]);
});

it('generates a hex string representing 32 bytes (64 nibbles)', () => {
const privateKey = electionCodeToPrivateKey('test');

expect(privateKey.length).to.eql(64);
})

context('when creating keys for a different curve size', () => {
it('generates a hex string representing 16 bytes (32 nibbles)', () => {
const privateKey = electionCodeToPrivateKey('test', 128);

expect(privateKey.length).to.eql(32);
})
})
})

41 changes: 41 additions & 0 deletions test/crypto/proof_of_election_code.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { expect } from "chai";
import { ProofOfElectionCodes } from "../../lib/av_client/crypto/proof_of_election_codes";
import { DiscreteLogarithmProof, Curve } from "../../lib/av_client/aion_crypto.js"
import { pointFromHex } from "../../lib/av_client/crypto/util";

describe("Proof of election codes", () => {

it("converts a single election code into a keyPair", () => {
const electionCode = 's3cr3t5';

const proof = new ProofOfElectionCodes([electionCode]);

expect(proof.mainKeyPair).to.eql({
privateKey: "631a1838f1e82b7b39f2b620a790de69ca8feb0cfd4ba984350a5fe3a2fda299",
publicKey: "021d1ccab6d4bc1e4cea12a13d291b2f5772f0c10e8ed4ac4c30be348137005759",
});
})

it("converts multiple election codes into a keyPair", () => {
const electionCodes = ['s3cr3t5','t0','k33p'];

const proof = new ProofOfElectionCodes(electionCodes);

expect(proof.mainKeyPair).to.eql({
privateKey: "e71d8edd52ff20ff8a741a9ae0f651193e183eafd639ebde02c847b2a35f9a8d",
publicKey: "020936bd7dacd0bfc974c717877fff2b0c4257a3ef0ea545b6acca9fa3e7857463"
});
})

it("generates a discrete logarithm proof", () => {
const electionCodes = ['s3cr3t5','t0','k33p'];

const proof = new ProofOfElectionCodes(electionCodes);
const discreteLogarithmProof = DiscreteLogarithmProof.fromString(proof.proof);

expect(proof.proof).to.exist;
const publicKey = pointFromHex(proof.mainKeyPair.publicKey).toEccPoint();
expect(discreteLogarithmProof.verifyWithoutChallenge(Curve.G, publicKey)).to.be.true;
})
})

102 changes: 102 additions & 0 deletions test/replies/otp_flow/get_b54bf489_3234d498d846_20_configuration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"electionConfig": {
"dbbPublicKey": "03a05ae5f22282d04ca5655bb5903d05c8f4e0d1ec37eeb34ba4c228f88c626156",
"encryptionKey": "0232c3623ad6dec442ac0284fe50fda0f96af187f0b1d36cbed065e3fbf1c5639f",
"thresholdConfig": {
"encryptionKey": "0232c3623ad6dec442ac0284fe50fda0f96af187f0b1d36cbed065e3fbf1c5639f",
"threshold": 1,
"trustees": [
{
"publicKey": "0232c3623ad6dec442ac0284fe50fda0f96af187f0b1d36cbed065e3fbf1c5639f",
"id": 22,
"polynomialCoefficients": []
}
]
},
"curveName": "secp256r1",
"services": {
"voterAuthorizer": {
"electionContextUuid": "a5ac8cfd-ba29-4016-b559-8abbdb8c9daf",
"publicKey": "0290a6759edfc186fd71077ba165edfb13df8f14b2741f96c7518ab1749a8e19c5",
"url": "http://localhost:3005/voting/b54bf489",
"authorizationMode": "proof-of-election-codes"
},
"otpProvider": {
"electionContextUuid": "a5ac8cfd-ba29-4016-b559-8abbdb8c9daf",
"publicKey": "0290a6759edfc186fd71077ba165edfb13df8f14b2741f96c7518ab1749a8e19c5",
"url": "http://localhost:3005/voting/b54bf489"
}
},
"ballotConfigs": {
"20": {
"reference": "20",
"voterGroup": "20",
"contestReferences": [
"20"
]
}
},
"contestConfigs": {
"20": {
"reference": "20",
"markingType": {
"blankSubmission": "active_choice",
"minMarks": 1,
"maxMarks": 1,
"encoding": {
"codeSize": 1,
"maxSize": 1,
"cryptogramCount": 1
}
},
"options": [
{
"reference": "2b6e8b8ed9b52fed",
"code": 1,
"title": {
"en": "It tolls for the fallen",
"da": "Den ringer for de faldne",
"de": "Es läutet für die Gefallenen"
}
},
{
"reference": "e30fe1fc5bc910ba",
"code": 2,
"title": {
"en": "It tolls for abandoned",
"da": "Den ringer for de efterladte",
"de": "Es läutet für die Verlassenen"
}
},
{
"reference": "2b842e4ff597b603",
"code": 3,
"title": {
"en": "It tolls for thee",
"da": "Den ringer for dig",
"de": "Es läutet für dich"
}
}
],
"title": {
"en": "Example ballot",
"da": "Eksempel valgseddel",
"de": "Beispielwahlzettel"
},
"resultType": {
"name": "regular"
}
}
},
"genesisConfig": {
"publicKey": "03a05ae5f22282d04ca5655bb5903d05c8f4e0d1ec37eeb34ba4c228f88c626156",
"authorizationMode": null,
"curveName": "secp256r1"
},
"latestConfigAddress": "f6021d9a93c4e9029c47034f5b1aac617ce6be0a1ffeaa291c2f659f9f430e9e",
"status": "open",
"sessionTimeout": 5400,
"bcTimeout": 600
},
"receipt": "73b2ba304b971e9512c682ca2ed8886ba9d883adc13fc1fc69022c3ec30f834d,2fbad62bf787943ceb3b9299aab3e56ac7720b9e517a5a0673708d6cf5a22561"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"castRequest": {
"address": "d98b752d71039dc3df90ab138768ba669a08fc9d7e0c2d8b2eeabfc3ab07c338",
"author": "voter-voter1",
"parentAddress": "5600090a77bb4f28592ebdc81684cf6cb55c53dbc10496447b70848e3eaac854",
"previousAddress": "5600090a77bb4f28592ebdc81684cf6cb55c53dbc10496447b70848e3eaac854",
"content": {},
"registeredAt": "2022-09-07T10:33:58.924Z",
"signature": "97b6e799db3b080c2d1ff8c8c8a2e21b0bed0b1f99efb895ea07d07d6a737f54,c03d61089244f654a88c1f041f7da9301a373ec4fbc342c8923fdfbcc1287c15",
"type": "CastRequestItem"
},
"receipt": "46e84599096bc60b29b5daff1cf6c023a1d390d853ca58f3d7c85fda741ac86b,b06fbdaa3a518d1b97cb20528e0c438926651eed2e382920617cd3c7bbbd6e88"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"voterCommitment": {
"address": "c17e7052c7a03d26f60cbe2b7e6f2c89218f041f6997b7363846d2bd8da246c6",
"author": "voter-voter1",
"parentAddress": "b46ac871df62bafaaa168206351f13125c4b9fab95940f57ee1eb50d5da6075e",
"previousAddress": "b46ac871df62bafaaa168206351f13125c4b9fab95940f57ee1eb50d5da6075e",
"content": {
"commitment": "0257dd5f15cc762d05505b7059aaf1fef8906fd17da6da7a634eddecac41107ed8"
},
"registeredAt": "2022-09-07T10:33:58.234Z",
"signature": "f8e5015846a9ca9746fad9558024d44ad85ea5d5dc994656d4d4a2d3b2b38608,befe98c896599e2c34657ea550eef07cb24b8e13609b754b275c5a770b9d1156",
"type": "VoterEncryptionCommitmentItem"
},
"boardCommitment": {
"address": "5ad13e017aed983ee275d1be1e53b5a92a36d676034a0570abdc4f7d247e3031",
"author": "Digital Ballot Box",
"parentAddress": "c17e7052c7a03d26f60cbe2b7e6f2c89218f041f6997b7363846d2bd8da246c6",
"previousAddress": "c17e7052c7a03d26f60cbe2b7e6f2c89218f041f6997b7363846d2bd8da246c6",
"content": {
"commitment": "03aba50419328423ea27434a71c5877bb928607ebc5e7b161095a2e11b90485a82"
},
"registeredAt": "2022-09-07T10:33:58.362Z",
"signature": "2eba8eda5ac77db6e2d0797ba5497dc833b414e6e89e88fd635d47b106cbc7f0,027f85dfc17795c61500bf7e753d0465f8f29b2cdd1267f5db33f88ea0c76790",
"type": "BoardEncryptionCommitmentItem"
},
"envelopes": {
"20": [
"02cde9f5187eb610fddfcd20f745a8a1f831810d67744ffbc1b01f7ae534ea8eb9,0255b4f9f7595ea66188eb0c7271cb1eaf30dca7d2b13d83c71643ddf97a3d21be"
]
},
"receipt": "7d15d68985283253847adf52f97239c5d904e12e0ff0930010a4e88ea22ff442,506a1c08952787dc11b9aad642b14414989d3aa7c0d5d3194e7361e7354e2f57"
}
Loading

0 comments on commit 1d11d39

Please sign in to comment.