Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ coverage/
.idea/
logs/
tsconfig.tsbuildinfo
out/
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
"dependencies": {
"@api-ts/io-ts-http": "^3.2.1",
"@api-ts/openapi-generator": "^5.7.0",
"@api-ts/typed-express-router": "^1.1.13",
"@api-ts/superagent-wrapper": "^1.3.3",
"@api-ts/response": "^2.1.0",
"@api-ts/typed-express-router": "^1.1.13",
"@bitgo/sdk-core": "^35.2.0",
"bitgo": "^48.0.0",
"body-parser": "^1.20.3",
Expand Down Expand Up @@ -51,6 +52,7 @@
"@types/sinon": "^10.0.11",
"@types/supertest": "^2.0.11",
"@types/winston": "^2.4.4",
"@types/superagent": "^8.1.9",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.0",
Expand Down
11 changes: 10 additions & 1 deletion src/__tests__/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import 'should';
import express from 'express';
import request from 'supertest';
import { setupRoutes } from '../routes/enclaved';
import { AppMode, TlsMode } from '../types';

describe('Routes', () => {
let app: express.Application;

beforeEach(() => {
app = express();
setupRoutes(app);
setupRoutes(app, {
appMode: AppMode.ENCLAVED,
tlsMode: TlsMode.DISABLED,
mtlsRequestCert: false,
kmsUrl: 'http://localhost:3000/kms',
timeout: 5000,
port: 3000,
bind: 'localhost',
});
});

describe('Health Check Routes', () => {
Expand Down
17 changes: 8 additions & 9 deletions src/api/enclaved/postIndependentKey.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { BitGo } from 'bitgo';
import * as express from 'express';
import { KmsClient } from '../../kms/kmsClient';
import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec';

export async function postIndependentKey(
req: express.Request,
res: express.Response,
): Promise<any> {
const { source, seed }: { source: string; seed?: string } = req.body;
req: EnclavedApiSpecRouteRequest<'v1.key.independent', 'post'>,
) {
const { source, seed }: { source: string; seed?: string } = req.decoded;
if (!source) {
throw new Error('Source is required for key generation');
}

// setup clients
const bitgo: BitGo = req.body.bitgo;
const bitgo: BitGo = req.bitgo;
const kms = new KmsClient();

// create public and private key pairs on BitGo SDK
Expand All @@ -34,9 +33,9 @@ export async function postIndependentKey(
seed,
});
} catch (error: any) {
res.status(error.status || 500).json({
throw {
status: error.status || 500,
message: error.message || 'Failed to post key to KMS',
});
return;
};
}
}
21 changes: 11 additions & 10 deletions src/api/enclaved/signMultisigTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as express from 'express';
import { KmsClient } from '../../kms/kmsClient';
import { BitGo, RequestTracer, TransactionPrebuild } from 'bitgo';
import { RequestTracer, TransactionPrebuild } from 'bitgo';
import logger from '../../logger';
import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec';

export async function signMultisigTransaction(
req: express.Request,
res: express.Response,
req: EnclavedApiSpecRouteRequest<'v1.multisig.sign', 'post'>,
): Promise<any> {
const {
source,
Expand All @@ -20,7 +19,7 @@ export async function signMultisigTransaction(
}

const reqId = new RequestTracer();
const bitgo: BitGo = req.body.bitgo;
const bitgo = req.bitgo;
const baseCoin = bitgo.coin(req.params.coin);
const kms = new KmsClient();

Expand All @@ -47,18 +46,20 @@ export async function signMultisigTransaction(
const res = await kms.getKey({ pub, source });
prv = res.prv;
} catch (error: any) {
res.status(error.status || 500).json({
throw {
status: error.status || 500,
message: error.message || 'Failed to retrieve key from KMS',
});
return;
};
}

// Sign the transaction using BitGo SDK
const coin = bitgo.coin(req.params.coin);
try {
return await coin.signTransaction({ txPrebuild, prv });
const signedTx = await coin.signTransaction({ txPrebuild, prv });
// The signed transaction format depends on the coin type
return signedTx;
} catch (error) {
console.log('error while signing wallet transaction ', error);
logger.error('error while signing wallet transaction:', error);
throw error;
}
}
2 changes: 1 addition & 1 deletion src/enclavedApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function app(cfg: EnclavedConfig): express.Application {
}

// Setup routes
setupRoutes(app);
setupRoutes(app, cfg);

// Add error handler
app.use(createErrorHandler());
Expand Down
125 changes: 125 additions & 0 deletions src/enclavedBitgoExpress/routers/enclavedApiSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import * as t from 'io-ts';
import {
apiSpec,
httpRoute,
httpRequest,
HttpResponse,
Method as HttpMethod,
} from '@api-ts/io-ts-http';
import {
createRouter,
type WrappedRouter,
TypedRequestHandler,
} from '@api-ts/typed-express-router';
import { Response } from '@api-ts/response';
import express from 'express';
import { BitGoRequest } from '../../types/request';
import { EnclavedConfig } from '../../types';
import { postIndependentKey } from '../../api/enclaved/postIndependentKey';
import { signMultisigTransaction } from '../../api/enclaved/signMultisigTransaction';
import { prepareBitGo, responseHandler } from '../../shared/middleware';

// Request type for /key/independent endpoint
const IndependentKeyRequest = {
source: t.string,
seed: t.union([t.undefined, t.string]),
};

// Response type for /key/independent endpoint
const IndependentKeyResponse: HttpResponse = {
// TODO: Define proper response type
200: t.any,
500: t.type({
error: t.string,
details: t.string,
}),
};

// Request type for /multisig/sign endpoint
const SignMultisigRequest = {
source: t.string,
pub: t.string,
txPrebuild: t.any, // TransactionPrebuild type from BitGo
};

// Response type for /multisig/sign endpoint
const SignMultisigResponse: HttpResponse = {
// TODO: Define proper response type for signed multisig transaction
200: t.any,
500: t.type({
error: t.string,
details: t.string,
}),
};

// API Specification
export const EnclavedAPiSpec = apiSpec({
'v1.multisig.sign': {
post: httpRoute({
method: 'POST',
path: '/{coin}/multisig/sign',
request: httpRequest({
params: {
coin: t.string,
},
body: SignMultisigRequest,
}),
response: SignMultisigResponse,
description: 'Sign a multisig transaction',
}),
},
'v1.key.independent': {
post: httpRoute({
method: 'POST',
path: '/{coin}/key/independent',
request: httpRequest({
params: {
coin: t.string,
},
body: IndependentKeyRequest,
}),
response: IndependentKeyResponse,
description: 'Generate an independent key',
}),
},
});

export type EnclavedApiSpecRouteHandler<
ApiName extends keyof typeof EnclavedAPiSpec,
Method extends keyof (typeof EnclavedAPiSpec)[ApiName] & HttpMethod,
> = TypedRequestHandler<typeof EnclavedAPiSpec, ApiName, Method>;

export type EnclavedApiSpecRouteRequest<
ApiName extends keyof typeof EnclavedAPiSpec,
Method extends keyof (typeof EnclavedAPiSpec)[ApiName] & HttpMethod,
> = BitGoRequest<EnclavedConfig> & Parameters<EnclavedApiSpecRouteHandler<ApiName, Method>>[0];

export type GenericEnclavedApiSpecRouteRequest = EnclavedApiSpecRouteRequest<any, any>;

// Create router with handlers
export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter<typeof EnclavedAPiSpec> {
const router = createRouter(EnclavedAPiSpec);
// Add middleware
router.use(express.json());
router.use(prepareBitGo(config));

// Independent key generation endpoint handler
router.post('v1.key.independent', [
responseHandler<EnclavedConfig>(async (req) => {
const typedReq = req as EnclavedApiSpecRouteRequest<'v1.key.independent', 'post'>;
const result = await postIndependentKey(typedReq);
return Response.ok(result);
}),
]);

// Multisig transaction signing endpoint handler
router.post('v1.multisig.sign', [
responseHandler<EnclavedConfig>(async (req) => {
const typedReq = req as EnclavedApiSpecRouteRequest<'v1.multisig.sign', 'post'>;
const result = await signMultisigTransaction(typedReq);
return Response.ok(result);
}),
]);

return router;
}
70 changes: 70 additions & 0 deletions src/enclavedBitgoExpress/routers/healthCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as t from 'io-ts';
import { apiSpec, httpRoute, httpRequest, HttpResponse } from '@api-ts/io-ts-http';
import { createRouter, type WrappedRouter } from '@api-ts/typed-express-router';
import { Response } from '@api-ts/response';
import pjson from '../../../package.json';
import { responseHandler } from '../../shared/middleware';

// Response type for /ping endpoint
const PingResponse: HttpResponse = {
200: t.type({
status: t.string,
timestamp: t.string,
}),
};

// Response type for /version endpoint
const VersionResponse: HttpResponse = {
200: t.type({
version: t.string,
name: t.string,
}),
};

// API Specification
export const HealthCheckApiSpec = apiSpec({
'v1.health.ping': {
post: httpRoute({
method: 'POST',
path: '/ping',
request: httpRequest({}),
response: PingResponse,
description: 'Health check endpoint that returns server status',
}),
},
'v1.health.version': {
get: httpRoute({
method: 'GET',
path: '/version',
request: httpRequest({}),
response: VersionResponse,
description: 'Returns the current version of the server',
}),
},
});

// Create router with handlers
export function createHealthCheckRouter(): WrappedRouter<typeof HealthCheckApiSpec> {
const router = createRouter(HealthCheckApiSpec);
// Ping endpoint handler
router.post('v1.health.ping', [
responseHandler(() =>
Response.ok({
status: 'enclaved express server is ok!',
timestamp: new Date().toISOString(),
}),
),
]);

// Version endpoint handler
router.get('v1.health.version', [
responseHandler(() =>
Response.ok({
version: pjson.version,
name: pjson.name,
}),
),
]);

return router;
}
7 changes: 7 additions & 0 deletions src/masterBitgoExpress/generateWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
import { createEnclavedExpressClient } from './enclavedExpressClient';
import _ from 'lodash';
import { MasterApiSpecRouteRequest } from './routers/masterApiSpec';
import { isMasterExpressConfig } from '../types';
import assert from 'assert';

/**
* This route is used to generate a multisig wallet when enclaved express is enabled
Expand All @@ -21,6 +23,11 @@ export async function handleGenerateWalletOnPrem(
const bitgo = req.bitgo;
const baseCoin = bitgo.coin(req.params.coin);

assert(
isMasterExpressConfig(req.config),
'Expected req.config to be of type MasterExpressConfig',
);

const enclavedExpressClient = createEnclavedExpressClient(req.config, req.params.coin);
if (!enclavedExpressClient) {
throw new Error(
Expand Down
4 changes: 2 additions & 2 deletions src/masterBitgoExpress/routers/enclavedExpressHealth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import https from 'https';
import superagent from 'superagent';
import { MasterExpressConfig, TlsMode } from '../../config';
import logger from '../../logger';
import { withResponseHandler } from '../../shared/responseHandler';
import { responseHandler } from '../../shared/middleware';

// Response type for /ping/enclavedExpress endpoint
const PingEnclavedResponse: HttpResponse = {
Expand Down Expand Up @@ -52,7 +52,7 @@ export function createEnclavedExpressRouter(

// Ping endpoint handler
router.post('v1.enclaved.ping', [
withResponseHandler(async () => {
responseHandler(async () => {
logger.debug('Pinging enclaved express');

try {
Expand Down
Loading