Skip to content

Commit

Permalink
Add optional (disabled by default) support for requiring new DIDs to …
Browse files Browse the repository at this point in the history
…complete a proof-of-work challenge before using the server
  • Loading branch information
finn-block committed Nov 8, 2023
1 parent 44f2c93 commit 0a6d2f4
Show file tree
Hide file tree
Showing 9 changed files with 508 additions and 133 deletions.
55 changes: 47 additions & 8 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
"@tbd54566975/dwn-sdk-js": "0.2.6",
"@tbd54566975/dwn-sql-store": "0.2.2",
"better-sqlite3": "^8.5.0",
"body-parser": "^1.20.2",
"bytes": "3.1.2",
"cors": "2.8.5",
"express": "4.18.2",
"kysely": "^0.26.3",
"loglevel": "^1.8.1",
"loglevel-plugin-prefix": "^0.8.4",
"multiformats": "11.0.2",
Expand Down
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export const config = {
eventLog:
process.env.DWN_STORAGE_EVENTS || process.env.DWN_STORAGE || 'level://data',

// require POW-based registration for new tenants
powRegistration: process.env.DWN_REGISTRATION_POW == 'true',
tenantRegistrationStore:
process.env.DWN_REGISTRATION_STORE ||
process.env.DWN_STORAGE ||
'sqlite://data/dwn.db',

// log level - trace/debug/info/warn/error
logLevel: process.env.DWN_SERVER_LOG_LEVEL || 'INFO',
};
16 changes: 12 additions & 4 deletions src/dwn-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import { HttpServerShutdownHandler } from './lib/http-server-shutdown-handler.js

import { type Config, config as defaultConfig } from './config.js';
import { HttpApi } from './http-api.js';
import { ProofOfWork } from './pow.js';
import { setProcessHandlers } from './process-handlers.js';
import { getDWNConfig } from './storage.js';
import { getDWNConfig, getDialectFromURI } from './storage.js';
import { WsApi } from './ws-api.js';

export type DwnServerOptions = {
Expand Down Expand Up @@ -50,8 +51,15 @@ export class DwnServer {
this.dwn = await Dwn.create(getDWNConfig(this.config));
}

this.#httpApi = new HttpApi(this.dwn);
this.#httpApi.start(this.config.port, () => {
let pow: ProofOfWork = null;
if (this.config.powRegistration) {
pow = new ProofOfWork(
getDialectFromURI(new URL(this.config.tenantRegistrationStore)),
);
}

this.#httpApi = new HttpApi(this.dwn, pow);
await this.#httpApi.start(this.config.port, () => {
log.info(`HttpServer listening on port ${this.config.port}`);
});

Expand All @@ -60,7 +68,7 @@ export class DwnServer {
);

if (this.config.webSocketServerEnabled) {
this.#wsApi = new WsApi(this.#httpApi.server, this.dwn);
this.#wsApi = new WsApi(this.#httpApi.server, this.dwn, pow);
this.#wsApi.start(() => log.info(`WebSocketServer ready...`));
}
}
Expand Down
32 changes: 30 additions & 2 deletions src/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,19 @@ import {

import { jsonRpcApi } from './json-rpc-api.js';
import { requestCounter, responseHistogram } from './metrics.js';
import type { ProofOfWork } from './pow.js';

export class HttpApi {
#api: Express;
#server: http.Server;
pow?: ProofOfWork;
dwn: Dwn;

constructor(dwn: Dwn) {
constructor(dwn: Dwn, pow?: ProofOfWork) {
this.#api = express();
this.#server = http.createServer(this.#api);
this.dwn = dwn;
this.pow = pow;

this.#setupMiddleware();
this.#setupRoutes();
Expand All @@ -47,6 +50,7 @@ export class HttpApi {

#setupMiddleware(): void {
this.#api.use(cors({ exposedHeaders: 'dwn-response' }));
this.#api.use(express.json());

this.#api.use(
responseTime((req: Request, res: Response, time) => {
Expand Down Expand Up @@ -81,6 +85,10 @@ export class HttpApi {
});

this.#api.get('/:did/records/:id', async (req, res) => {
if (this.pow && !(await this.pow.isAuthorized(req.params.did))) {
return res.status(403).json('did not authorized on this server');
}

const record = await RecordsRead.create({
filter: { recordId: req.params.id },
});
Expand Down Expand Up @@ -142,6 +150,19 @@ export class HttpApi {
return res.status(400).json(reply);
}

if (
this.pow &&
!(await this.pow.isAuthorized(dwnRpcRequest.params.target))
) {
const reply = createJsonRpcErrorResponse(
dwnRpcRequest.id || uuidv4(),
JsonRpcErrorCodes.Forbidden,
'tenant not authorized, please register first',
);

return res.status(403).json(reply);
}

// Check whether data was provided in the request body
const contentLength = req.headers['content-length'];
const transferEncoding = req.headers['transfer-encoding'];
Expand Down Expand Up @@ -181,13 +202,20 @@ export class HttpApi {
return res.json(jsonRpcResponse);
}
});

if (this.pow) {
this.pow.setupRoutes(this.#api);
}
}

#listen(port: number, callback?: () => void): void {
this.#server.listen(port, callback);
}

start(port: number, callback?: () => void): http.Server {
async start(port: number, callback?: () => void): Promise<http.Server> {
if (this.pow) {
await this.pow.initialize();
}
this.#listen(port, callback);
return this.#server;
}
Expand Down
119 changes: 119 additions & 0 deletions src/pow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { createHash } from 'crypto';
import type { Request, Response } from 'express';
import type { Express } from 'express';
import type { Dialect } from 'kysely';
import { Kysely } from 'kysely';

const recentChallenges: { [challenge: string]: number } = {};
const CHALLENGE_TIMEOUT = 60 * 1000;

export class ProofOfWork {
#db: Kysely<PowDatabase>;

constructor(dialect: Dialect) {
this.#db = new Kysely<PowDatabase>({ dialect: dialect });
}

async initialize(): Promise<void> {
setInterval(() => {
for (const challenge of Object.keys(recentChallenges)) {
if (
recentChallenges[challenge] &&
Date.now() - recentChallenges[challenge] > CHALLENGE_TIMEOUT
) {
delete recentChallenges[challenge];
}
}
}, CHALLENGE_TIMEOUT / 4);

await this.#db.schema
.createTable('authorizedTenants')
.ifNotExists()
.addColumn('did', 'text', (column) => column.primaryKey())
.execute();
}

setupRoutes(server: Express): void {
server.get('/register', (req: Request, res: Response) =>
this.getChallenge(req, res),
);
server.post('/register', (req: Request, res: Response) =>
this.verifyChallenge(req, res),
);
}

async isAuthorized(tenant: string): Promise<boolean> {
const result = await this.#db
.selectFrom('authorizedTenants')
.select('did')
.where('did', '=', tenant)
.execute();

return result.length > 0;
}

private async getChallenge(_req: Request, res: Response): Promise<void> {
const challenge = generateChallenge();
recentChallenges[challenge] = Date.now();
res.json({
challenge: challenge,
complexity: getComplexity(),
});
}

private async verifyChallenge(req: Request, res: Response): Promise<void> {
const body: {
did: string;
challenge: string;
response: string;
} = req.body;

const hash = createHash('sha256');
hash.update(body.challenge);
hash.update(body.response);

const complexity = getComplexity();
const digest = hash.digest('hex');
console.log('digest: ', digest);
if (!digest.startsWith('0'.repeat(complexity))) {
res.status(401).json({ success: false });
return;
}

try {
await this.#db
.insertInto('authorizedTenants')
.values({ did: body.did })
.executeTakeFirst();
} catch (e) {
console.log('error inserting did', e);
res.status(500).json({ success: false });
return;
}
res.json({ success: true });
}
}

const challengeCharacters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

function generateChallenge(): string {
let challenge = '';
while (challenge.length < 10) {
challenge += challengeCharacters.charAt(
Math.floor(Math.random() * challengeCharacters.length),
);
}
return challenge;
}

function getComplexity(): number {
return Object.keys(recentChallenges).length;
}
interface AuthorizedTenants {
did: string;
}

interface PowDatabase {
authorizedTenants: AuthorizedTenants;
}
4 changes: 2 additions & 2 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,14 @@ function getStore(storeString: string, storeType: EStoreType): StoreType {
case BackendTypes.SQLITE:
case BackendTypes.MYSQL:
case BackendTypes.POSTGRES:
return getDBStore(getDBFromURI(storeURI), storeType);
return getDBStore(getDialectFromURI(storeURI), storeType);

default:
throw invalidStorageSchemeMessage(storeURI.protocol);
}
}

function getDBFromURI(u: URL): Dialect {
export function getDialectFromURI(u: URL): Dialect {
switch (u.protocol.slice(0, -1)) {
case BackendTypes.SQLITE:
return new SqliteDialect({
Expand Down
Loading

0 comments on commit 0a6d2f4

Please sign in to comment.