Skip to content

Commit

Permalink
Merge pull request #1507 from privacy-scaling-explorations/feature/co…
Browse files Browse the repository at this point in the history
…ordinator-public-key

feat: coordinator public key method
  • Loading branch information
0xmad authored May 24, 2024
2 parents ee2c2ec + 36dbd4f commit d0b5add
Show file tree
Hide file tree
Showing 16 changed files with 411 additions and 129 deletions.
54 changes: 24 additions & 30 deletions coordinator/tests/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { App } from "supertest/types";
import { AppModule } from "../ts/app.module";
import { ErrorCodes } from "../ts/common";
import { CryptoService } from "../ts/crypto/crypto.service";
import { FileModule } from "../ts/file/file.module";

const STATE_TREE_DEPTH = 10;
const INT_STATE_TREE_DEPTH = 1;
Expand All @@ -40,11 +41,13 @@ describe("AppController (e2e)", () => {
let maciAddresses: DeployedContracts;
let pollContracts: PollContracts;

const cryptoService = new CryptoService();

const getAuthorizationHeader = async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const signature = await signer.signMessage("message");
const digest = Buffer.from(getBytes(hashMessage("message"))).toString("hex");
return `Bearer ${CryptoService.getInstance().encrypt(publicKey, `${signature}:${digest}`)}`;
return `Bearer ${cryptoService.encrypt(publicKey, `${signature}:${digest}`)}`;
};

beforeAll(async () => {
Expand Down Expand Up @@ -88,7 +91,7 @@ describe("AppController (e2e)", () => {

beforeEach(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
imports: [AppModule, FileModule],
}).compile();

app = moduleFixture.createNestApplication();
Expand Down Expand Up @@ -117,10 +120,7 @@ describe("AppController (e2e)", () => {

test("should throw an error if poll id is invalid", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize());
const encryptedHeader = await getAuthorizationHeader();

const result = await request(app.getHttpServer() as App)
Expand Down Expand Up @@ -166,10 +166,7 @@ describe("AppController (e2e)", () => {

test("should throw an error if maci address is invalid", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize());
const encryptedHeader = await getAuthorizationHeader();

const result = await request(app.getHttpServer() as App)
Expand All @@ -193,10 +190,7 @@ describe("AppController (e2e)", () => {

test("should throw an error if tally address is invalid", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize());
const encryptedHeader = await getAuthorizationHeader();

const result = await request(app.getHttpServer() as App)
Expand All @@ -219,6 +213,18 @@ describe("AppController (e2e)", () => {
});
});

describe("/v1/proof/publicKey GET", () => {
test("should get public key properly", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);

const result = await request(app.getHttpServer() as App)
.get("/v1/proof/publicKey")
.expect(200);

expect(result.body).toStrictEqual({ publicKey: publicKey.toString() });
});
});

describe("/v1/proof/generate POST", () => {
beforeAll(async () => {
const user = new Keypair();
Expand All @@ -240,10 +246,7 @@ describe("AppController (e2e)", () => {

test("should throw an error if poll is not over", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize());
const encryptedHeader = await getAuthorizationHeader();

const result = await request(app.getHttpServer() as App)
Expand All @@ -266,10 +269,7 @@ describe("AppController (e2e)", () => {

test("should throw an error if signups are not merged", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize());
const encryptedHeader = await getAuthorizationHeader();

const result = await request(app.getHttpServer() as App)
Expand All @@ -295,10 +295,7 @@ describe("AppController (e2e)", () => {
await mergeSignups({ pollId: 0n, signer });

const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize());
const encryptedHeader = await getAuthorizationHeader();

const result = await request(app.getHttpServer() as App)
Expand Down Expand Up @@ -405,10 +402,7 @@ describe("AppController (e2e)", () => {

test("should generate proofs properly", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize());
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
Expand Down
34 changes: 33 additions & 1 deletion coordinator/ts/app.controller.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { HttpException, HttpStatus } from "@nestjs/common";
import { Test } from "@nestjs/testing";

import type { IGetPublicKeyData } from "./file/types";
import type { IGenerateArgs, IGenerateData } from "./proof/types";
import type { TallyData } from "maci-cli";

import { AppController } from "./app.controller";
import { FileService } from "./file/file.service";
import { ProofGeneratorService } from "./proof/proof.service";

describe("AppController", () => {
Expand All @@ -25,10 +27,18 @@ describe("AppController", () => {
tallyData: {} as TallyData,
};

const defaultPublicKeyData: IGetPublicKeyData = {
publicKey: "key",
};

const mockGeneratorService = {
generate: jest.fn(),
};

const mockFileService = {
getPublicKey: jest.fn(),
};

beforeEach(async () => {
const app = await Test.createTestingModule({
controllers: [AppController],
Expand All @@ -40,6 +50,12 @@ describe("AppController", () => {
return mockGeneratorService;
}

if (token === FileService) {
mockFileService.getPublicKey.mockResolvedValue(defaultPublicKeyData);

return mockFileService;
}

return jest.fn();
})
.compile();
Expand All @@ -51,7 +67,7 @@ describe("AppController", () => {
jest.clearAllMocks();
});

describe("v1/proof", () => {
describe("v1/proof/generate", () => {
test("should return generated proof data", async () => {
const data = await appController.generate(defaultProofGeneratorArgs);
expect(data).toStrictEqual(defaultProofGeneratorData);
Expand All @@ -66,4 +82,20 @@ describe("AppController", () => {
);
});
});

describe("v1/proof/publicKey", () => {
test("should return public key properly", async () => {
const data = await appController.getPublicKey();
expect(data).toStrictEqual(defaultPublicKeyData);
});

test("should throw an error if file service throws an error", async () => {
const error = new Error("error");
mockFileService.getPublicKey.mockRejectedValue(error);

await expect(appController.getPublicKey()).rejects.toThrow(
new HttpException(error.message, HttpStatus.BAD_REQUEST),
);
});
});
});
31 changes: 27 additions & 4 deletions coordinator/ts/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Body, Controller, HttpException, HttpStatus, Logger, Post, UseGuards } from "@nestjs/common";
import { Body, Controller, Get, HttpException, HttpStatus, Logger, Post, UseGuards } from "@nestjs/common";
import { ApiBearerAuth, ApiBody, ApiResponse, ApiTags } from "@nestjs/swagger";

import { AccountSignatureGuard } from "./auth/AccountSignatureGuard.service";
import type { IGetPublicKeyData } from "./file/types";
import type { IGenerateData } from "./proof/types";

import { AccountSignatureGuard, Public } from "./auth/AccountSignatureGuard.service";
import { FileService } from "./file/file.service";
import { GenerateProofDto } from "./proof/dto";
import { ProofGeneratorService } from "./proof/proof.service";
import { IGenerateData } from "./proof/types";

@ApiTags("v1/proof")
@ApiBearerAuth()
Expand All @@ -20,8 +23,12 @@ export class AppController {
* Initialize AppController
*
* @param proofGeneratorService - proof generator service
* @param fileService - file service
*/
constructor(private readonly proofGeneratorService: ProofGeneratorService) {}
constructor(
private readonly proofGeneratorService: ProofGeneratorService,
private readonly fileService: FileService,
) {}

/**
* Generate proofs api method
Expand All @@ -40,4 +47,20 @@ export class AppController {
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
});
}

/**
* Get RSA public key for authorization setup
*
* @returns RSA public key
*/
@ApiResponse({ status: HttpStatus.OK, description: "Public key was successfully returned" })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "BadRequest" })
@Public()
@Get("publicKey")
async getPublicKey(): Promise<IGetPublicKeyData> {
return this.fileService.getPublicKey().catch((error: Error) => {
this.logger.error(`Error:`, error);
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
});
}
}
4 changes: 4 additions & 0 deletions coordinator/ts/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Module } from "@nestjs/common";
import { ThrottlerModule } from "@nestjs/throttler";

import { AppController } from "./app.controller";
import { CryptoModule } from "./crypto/crypto.module";
import { FileModule } from "./file/file.module";
import { ProofGeneratorService } from "./proof/proof.service";

@Module({
Expand All @@ -12,6 +14,8 @@ import { ProofGeneratorService } from "./proof/proof.service";
limit: Number(process.env.LIMIT),
},
]),
FileModule,
CryptoModule,
],
controllers: [AppController],
providers: [ProofGeneratorService],
Expand Down
42 changes: 35 additions & 7 deletions coordinator/ts/auth/AccountSignatureGuard.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Logger, CanActivate, type ExecutionContext, Injectable } from "@nestjs/common";
import {
Logger,
CanActivate,
Injectable,
SetMetadata,
type ExecutionContext,
type CustomDecorator,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { ethers } from "ethers";

import fs from "fs";
Expand All @@ -8,6 +16,18 @@ import type { Request as Req } from "express";

import { CryptoService } from "../crypto/crypto.service";

/**
* Public metadata key
*/
export const PUBLIC_METADATA_KEY = "isPublic";

/**
* Public decorator to by-pass auth checks
*
* @returns public decorator
*/
export const Public = (): CustomDecorator => SetMetadata(PUBLIC_METADATA_KEY, true);

/**
* AccountSignatureGuard is responsible for protecting calling controller functions.
* If account address is not added to .env file, you will not be allowed to call any API methods.
Expand All @@ -24,15 +44,17 @@ import { CryptoService } from "../crypto/crypto.service";
*/
@Injectable()
export class AccountSignatureGuard implements CanActivate {
/**
* Crypto service
*/
private readonly cryptoService = CryptoService.getInstance();

/**
* Logger
*/
private readonly logger = new Logger(AccountSignatureGuard.name);
private readonly logger: Logger;

constructor(
private readonly cryptoService: CryptoService,
private readonly reflector: Reflector,
) {
this.logger = new Logger(AccountSignatureGuard.name);
}

/**
* This function should return a boolean, indicating whether the request is allowed or not based on message signature and digest.
Expand All @@ -42,6 +64,12 @@ export class AccountSignatureGuard implements CanActivate {
*/
async canActivate(ctx: ExecutionContext): Promise<boolean> {
try {
const isPublic = this.reflector.get<boolean>(PUBLIC_METADATA_KEY, ctx.getHandler());

if (isPublic) {
return true;
}

const request = ctx.switchToHttp().getRequest<Req>();
const encryptedHeader = request.headers.authorization;

Expand Down
Loading

0 comments on commit d0b5add

Please sign in to comment.