Skip to content

Commit

Permalink
Integrated RegistrationStore with RegistrationManager + added main-li…
Browse files Browse the repository at this point in the history
…ne scenario tests
  • Loading branch information
thehenrytsai committed Jan 8, 2024
1 parent 75eb96f commit 1ba211e
Show file tree
Hide file tree
Showing 16 changed files with 643 additions and 380 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"url": "https://github.com/TBD54566975/dwn-server/issues"
},
"dependencies": {
"@tbd54566975/dwn-sdk-js": "0.2.10",
"@tbd54566975/dwn-sdk-js": "0.2.11",
"@tbd54566975/dwn-sql-store": "0.2.5",
"better-sqlite3": "^8.5.0",
"body-parser": "^1.20.2",
Expand Down
13 changes: 13 additions & 0 deletions src/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ export class DwnServerError extends Error {

this.name = 'DwnServerError';
}

/**
* Called by `JSON.stringify(...)` automatically.
*/
public toJSON(): { code: string, message: string } {
return {
code: this.code,
message: this.message,
};
}
}

/**
Expand All @@ -18,5 +28,8 @@ export class DwnServerError extends Error {
export enum DwnServerErrorCode {
ProofOfWorkInsufficientSolutionNonce = 'ProofOfWorkInsufficientSolutionNonce',
ProofOfWorkInvalidOrExpiredChallenge = 'ProofOfWorkInvalidOrExpiredChallenge',
ProofOfWorkManagerInvalidChallengeNonce = 'ProofOfWorkManagerInvalidChallengeNonce',
ProofOfWorkManagerInvalidResponseNonceFormat = 'ProofOfWorkManagerInvalidResponseNonceFormat',
RegistrationManagerInvalidOrOutdatedTermsOfServiceHash = 'RegistrationManagerInvalidOrOutdatedTermsOfServiceHash',
TenantRegistrationOutdatedTermsOfService = 'TenantRegistrationOutdatedTermsOfService',
}
25 changes: 15 additions & 10 deletions src/dwn-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { setProcessHandlers } from './process-handlers.js';
import { RegisteredTenantGate } from './registered-tenant-gate.js';
import { getDWNConfig, getDialectFromURI } from './storage.js';
import { WsApi } from './ws-api.js';
import { RegistrationManager } from './registration/registration-manager.js';

export type DwnServerOptions = {
dwn?: Dwn;
Expand Down Expand Up @@ -48,28 +49,32 @@ export class DwnServer {
* The DWN creation is secondary and only happens if it hasn't already been done.
*/
async #setupServer(): Promise<void> {
// Load terms of service if given the path.
const termsOfService =
this.config.termsOfServiceFilePath !== undefined
? readFileSync(this.config.termsOfServiceFilePath).toString()
: undefined;

const tenantGateDB = getDialectFromURI(
new URL(this.config.tenantRegistrationStore),
);

let tenantGate: RegisteredTenantGate;
let registrationManager: RegistrationManager;
if (!this.dwn) {
const tenantGateDB = getDialectFromURI(
new URL(this.config.tenantRegistrationStore),
);

// Load terms of service if given the path.
const termsOfService =
this.config.termsOfServiceFilePath !== undefined
? readFileSync(this.config.termsOfServiceFilePath).toString()
: undefined;

tenantGate = new RegisteredTenantGate(
tenantGateDB,
this.config.registrationProofOfWorkEnabled,
termsOfService,
);
registrationManager = await RegistrationManager.create({ sqlDialect: tenantGateDB, termsOfService });

this.dwn = await Dwn.create(getDWNConfig(this.config, tenantGate));
}

this.#httpApi = new HttpApi(this.config, this.dwn, tenantGate);
this.#httpApi = new HttpApi(this.config, this.dwn, tenantGate, registrationManager);

await this.#httpApi.start(this.config.port, () => {
log.info(`HttpServer listening on port ${this.config.port}`);
});
Expand Down
29 changes: 4 additions & 25 deletions src/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ export class HttpApi {
registrationManager: RegistrationManager;
dwn: Dwn;

constructor(config: Config, dwn: Dwn, tenantGate: RegisteredTenantGate) {
constructor(config: Config, dwn: Dwn, tenantGate: RegisteredTenantGate, registrationManager: RegistrationManager) {
this.#config = config;
this.#api = express();
this.#server = http.createServer(this.#api);
this.dwn = dwn;
this.tenantGate = tenantGate;
this.registrationManager = registrationManager;

this.#setupMiddleware();
this.#setupRoutes();
Expand Down Expand Up @@ -195,28 +196,9 @@ export class HttpApi {
#setupRegistrationRoutes(): void {
if (this.#config.registrationProofOfWorkEnabled) {
this.#api.get('/register/proof-of-work', async (_req: Request, res: Response) => {
const proofOfWorkChallenge = await this.tenantGate.getProofOfWorkChallenge();
const proofOfWorkChallenge = await this.registrationManager.getProofOfWorkChallenge();
res.json(proofOfWorkChallenge);
});

this.#api.post('/register/proof-of-work', async (req: Request, res: Response) => {
try {
await this.tenantGate.handleProofOfWorkChallengePost(req.body);
res.json({ success: true });
} catch (error) {
const dwnServerError = error as DwnServerError;

if (dwnServerError.code !== undefined) {
res.status(401).json({
success : false,
reason : dwnServerError.message,
});
} else {
console.log('Error handling proof-of-work POST:', error);
res.status(500).json({ success: false });
}
}
});
}
if (this.#config.termsOfServiceFilePath !== undefined) {
this.#api.get('/register/terms-of-service', (_req: Request, res: Response) => res.send(this.tenantGate.termsOfService));
Expand Down Expand Up @@ -248,10 +230,7 @@ export class HttpApi {
const dwnServerError = error as DwnServerError;

if (dwnServerError.code !== undefined) {
res.status(400).json({
success : false,
reason : dwnServerError.message,
});
res.status(400).json(dwnServerError);
} else {
console.log('Error handling registration request:', error);
res.status(500).json({ success: false });
Expand Down
59 changes: 3 additions & 56 deletions src/registered-tenant-gate.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { TenantGate } from '@tbd54566975/dwn-sdk-js';

import { createHash, randomBytes } from 'crypto';
import type { Dialect } from 'kysely';
import { Kysely } from 'kysely';

import { DwnServerError } from './dwn-error.js';
import { DwnServerErrorCode } from './dwn-error.js';
import { ProofOfWork } from './registration/proof-of-work.js';
import type { ProofOfWorkChallengeModel } from './registration/proof-of-work-types.js';

const recentChallenges: { [challenge: string]: number } = {};
const recentChallenges: { [challengeNonce: string]: number } = {};
const CHALLENGE_TIMEOUT = 5 * 60 * 1000; // challenges are valid this long after issuance
const COMPLEXITY_LOOKBACK = 5 * 60 * 1000; // complexity is based on number of successful registrations in this time frame
const COMPLEXITY_MINIMUM = BigInt('0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF');
Expand All @@ -29,9 +27,7 @@ export class RegisteredTenantGate implements TenantGate {
this.#proofOfWorkRequired = proofOfWorkRequired;

if (termsOfService) {
const termsOfServiceHash = createHash('sha256');
termsOfServiceHash.update(termsOfService);
this.#termsOfServiceHash = termsOfServiceHash.digest('hex');
this.#termsOfServiceHash = ProofOfWork.hashAsHexString([termsOfService]);
this.#termsOfService = termsOfService;
}
}
Expand All @@ -54,7 +50,7 @@ export class RegisteredTenantGate implements TenantGate {
.execute();
}

async isTenant(tenant: string): Promise<boolean> {
async isActiveTenant(tenant: string): Promise<boolean> {
if (!this.#proofOfWorkRequired && !this.#termsOfService) {
return true;
}
Expand All @@ -81,55 +77,6 @@ export class RegisteredTenantGate implements TenantGate {
return true;
}

async authorizeTenantProofOfWork(tenant: string): Promise<void> {
await this.#db
.insertInto('authorizedTenants')
.values({
did: tenant,
proofOfWorkTime: Date.now(),
})
.onConflict((oc) =>
oc.column('did').doUpdateSet((eb) => ({
proofOfWorkTime: eb.ref('excluded.proofOfWorkTime'),
})),
)
.executeTakeFirst();
}

async getProofOfWorkChallenge(): Promise<ProofOfWorkChallengeModel> {
const challenge = randomBytes(10).toString('base64');
recentChallenges[challenge] = Date.now();
return {
challenge: challenge,
complexity: this.bigIntToHexString(await this.getComplexity()),
};
}

private bigIntToHexString (int: BigInt): string {
let hex = int.toString(16).toUpperCase();
const stringLength = hex.length;
for (let pad = stringLength; pad < 64; pad++) {
hex = '0' + hex;
}
return hex;
}

async handleProofOfWorkChallengePost(body: { did: string; challenge: string; response: string }): Promise<void> {
const challengeIssued = recentChallenges[body.challenge];

if (challengeIssued == undefined || Date.now() - challengeIssued > CHALLENGE_TIMEOUT) {
throw new DwnServerError(DwnServerErrorCode.ProofOfWorkInvalidOrExpiredChallenge, `Invalid or expired challenge: ${body.challenge}.`);
}

ProofOfWork.verifyChallengeResponse({
challenge: body.challenge,
responseNonce: body.response,
maximumAllowedHashValue: await this.getComplexity(),
});

await this.authorizeTenantProofOfWork(body.did);
}

private async getComplexity(): Promise<bigint> {
const result = await this.#db
.selectFrom('authorizedTenants')
Expand Down
Loading

0 comments on commit 1ba211e

Please sign in to comment.