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 .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,4 @@ jobs:
MTLS_ENABLED: true
MTLS_REQUEST_CERT: true
MTLS_REJECT_UNAUTHORIZED: false
KMS_URL: "https://localhost:3000/"
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@
"generate-test-ssl": "openssl req -x509 -newkey rsa:2048 -keyout test-ssl-key.pem -out test-ssl-cert.pem -days 365 -nodes -subj '/CN=localhost'"
},
"dependencies": {
"bitgo": "^44.2.0",
"@bitgo/sdk-core": "^33.2.0",
"bitgo": "^44.2.0",
"body-parser": "^1.20.3",
"connect-timeout": "^1.9.0",
"debug": "^3.1.0",
"express": "4.17.3",
"lodash": "^4.17.20",
"morgan": "^1.9.1",
"superagent": "^8.0.9",
"proxy-agent": "6.4.0",
"proxyquire": "^2.1.3"
"proxyquire": "^2.1.3",
"superagent": "^8.0.9",
"zod": "^3.25.48"
},
"devDependencies": {
"@types/body-parser": "^1.17.0",
Expand Down
42 changes: 42 additions & 0 deletions src/api/enclaved/postIndependentKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BitGo } from 'bitgo';
import * as express from 'express';
import { KmsClient } from '../../kms/kmsClient';

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

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

// create public and private key pairs on BitGo SDK
const coin = bitgo.coin(req.params.coin);
const { pub, prv } = coin.keychains().create();

if (!pub) {
throw new Error('BitGo SDK failed to create public key');
}

// post key to KMS for encryption and storage
try {
return await kms.postKey({
pub,
prv,
coin: req.params.coin,
source,
type: 'independent',
seed,
});
} catch (error: any) {
res.status(error.status || 500).json({
message: error.message || 'Failed to post key to KMS',
});
return;
}
}
10 changes: 10 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const defaultEnclavedConfig: EnclavedConfig = {
bind: 'localhost',
timeout: 305 * 1000,
logFile: '',
kmsUrl: '', // Will be overridden by environment variable
tlsMode: TlsMode.ENABLED,
mtlsRequestCert: false,
mtlsRejectUnauthorized: false,
Expand All @@ -68,6 +69,12 @@ function determineTlsMode(): TlsMode {
}

function enclavedEnvConfig(): Partial<EnclavedConfig> {
const kmsUrl = readEnvVar('KMS_URL');

if (!kmsUrl) {
throw new Error('KMS_URL environment variable is required and cannot be empty');
}

return {
appMode: AppMode.ENCLAVED,
port: Number(readEnvVar('MASTER_BITGO_EXPRESS_PORT')),
Expand All @@ -80,6 +87,8 @@ function enclavedEnvConfig(): Partial<EnclavedConfig> {
timeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_TIMEOUT')),
keepAliveTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_KEEP_ALIVE_TIMEOUT')),
headersTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_HEADERS_TIMEOUT')),
// KMS settings
kmsUrl,
// TLS settings
keyPath: readEnvVar('MASTER_BITGO_EXPRESS_KEYPATH'),
crtPath: readEnvVar('MASTER_BITGO_EXPRESS_CRTPATH'),
Expand Down Expand Up @@ -112,6 +121,7 @@ function mergeEnclavedConfigs(...configs: Partial<EnclavedConfig>[]): EnclavedCo
timeout: get('timeout'),
keepAliveTimeout: get('keepAliveTimeout'),
headersTimeout: get('headersTimeout'),
kmsUrl: get('kmsUrl'),
keyPath: get('keyPath'),
crtPath: get('crtPath'),
tlsKey: get('tlsKey'),
Expand Down
44 changes: 44 additions & 0 deletions src/kms/kmsClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import debug from 'debug';
import * as superagent from 'superagent';
import { config, isMasterExpressConfig } from '../config';
import { PostKeyKmsSchema, PostKeyParams, PostKeyResponse } from './types/postKey';

const debugLogger = debug('bitgo:express:kmsClient');

export class KmsClient {
private readonly url: string;

constructor() {
const cfg = config();
if (isMasterExpressConfig(cfg)) {
throw new Error('Configuration is not in enclaved express mode');
}

if (!cfg.kmsUrl) {
throw new Error('KMS URL not configured. Please set KMS_URL in your environment.');
}

this.url = cfg.kmsUrl;
debugLogger('kmsClient initialized with URL: %s', this.url);
}

async postKey(params: PostKeyParams): Promise<PostKeyResponse> {
debugLogger('Posting key to KMS: %O', params);

const kmsResponse = await superagent
.post(`${this.url}/key`)
.set('x-api-key', 'abc')
.send(params);

try {
PostKeyKmsSchema.parse(kmsResponse.body);
} catch (error: any) {
throw new Error(
`KMS returned unexpected response${error.message ? `: ${error.message}` : ''}`,
);
}

const { pub, coin, source } = kmsResponse.body;
return { pub, coin, source } as PostKeyResponse;
}
}
24 changes: 24 additions & 0 deletions src/kms/types/postKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as z from 'zod';

export interface PostKeyParams {
prv: string;
pub: string;
coin: string;
source: string;
type: 'independent' | 'enclaved';
seed?: string; // Optional seed for key generation
}

export interface PostKeyResponse {
pub: string;
coin: string;
source: string;
type: 'independent' | 'enclaved';
}

export const PostKeyKmsSchema = z.object({
pub: z.string(),
coin: z.string(),
source: z.enum(['independent', 'tss']),
type: z.enum(['user', 'backup']),
});
14 changes: 12 additions & 2 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import express from 'express';
import debug from 'debug';
import pjson from '../package.json';
import { BitGo, BitGoOptions } from 'bitgo';
import { postIndependentKey } from './api/enclaved/postIndependentKey';

const debugLogger = debug('enclaved:routes');

Expand Down Expand Up @@ -37,8 +39,16 @@ function setupPingRoutes(app: express.Application) {
app.get('/version', promiseWrapper(handleVersionInfo));
}

function setupKeyGenRoutes() {
function prepBitGo(req: express.Request, res: express.Response, next: express.NextFunction) {
const bitgoConstructorParams: BitGoOptions = {};
req.body.bitgo = new BitGo(bitgoConstructorParams);

next();
}

function setupKeyGenRoutes(app: express.Application) {
// Register additional routes here as needed
app.post('/:coin/key/independentKey', prepBitGo, promiseWrapper(postIndependentKey));
debugLogger('KeyGen routes configured');
}

Expand All @@ -51,7 +61,7 @@ export function setupRoutes(app: express.Application): void {
setupPingRoutes(app);

// Register keygen routes
setupKeyGenRoutes();
setupKeyGenRoutes(app);

// Add a catch-all for unsupported routes
app.use('*', (_req, res) => {
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ interface BaseConfig {
// Enclaved mode specific configuration
export interface EnclavedConfig extends BaseConfig {
appMode: AppMode.ENCLAVED;
// KMS settings
kmsUrl: string;
// TLS settings
keyPath?: string;
crtPath?: string;
Expand Down
Loading