Skip to content

Commit d88fbc5

Browse files
committed
feat(ebe): KMS client and post independent key API
added a KMS client for EBE to connect to added API for EBE to post independent keys TICKET: WP_4374
1 parent 78ef37e commit d88fbc5

File tree

9 files changed

+683
-545
lines changed

9 files changed

+683
-545
lines changed

.github/workflows/build-and-test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@ jobs:
7575
MTLS_ENABLED: true
7676
MTLS_REQUEST_CERT: true
7777
MTLS_REJECT_UNAUTHORIZED: false
78+
KMS_URL: "https://localhost:3000/"

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@
1717
"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'"
1818
},
1919
"dependencies": {
20-
"bitgo": "^44.2.0",
2120
"@bitgo/sdk-core": "^33.2.0",
21+
"bitgo": "^44.2.0",
2222
"body-parser": "^1.20.3",
2323
"connect-timeout": "^1.9.0",
2424
"debug": "^3.1.0",
2525
"express": "4.17.3",
2626
"lodash": "^4.17.20",
2727
"morgan": "^1.9.1",
28-
"superagent": "^8.0.9",
2928
"proxy-agent": "6.4.0",
30-
"proxyquire": "^2.1.3"
29+
"proxyquire": "^2.1.3",
30+
"superagent": "^8.0.9",
31+
"zod": "^3.25.48"
3132
},
3233
"devDependencies": {
3334
"@types/body-parser": "^1.17.0",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { BitGo } from 'bitgo';
2+
import * as express from 'express';
3+
import { KmsClient } from '../../kms/kmsClient';
4+
5+
export async function postIndependentKey(
6+
req: express.Request,
7+
res: express.Response,
8+
): Promise<any> {
9+
const { source, seed }: { source: string; seed?: string } = req.body;
10+
if (!source) {
11+
throw new Error('Source is required for key generation');
12+
}
13+
14+
// setup clients
15+
const bitgo: BitGo = req.body.bitgo;
16+
const kms = new KmsClient();
17+
18+
// create public and private key pairs on BitGo SDK
19+
const coin = bitgo.coin(req.params.coin);
20+
const { pub, prv } = coin.keychains().create();
21+
22+
if (!pub) {
23+
throw new Error('BitGo SDK failed to create public key');
24+
}
25+
26+
// post key to KMS for encryption and storage
27+
try {
28+
return await kms.postKey({
29+
pub,
30+
prv,
31+
coin: req.params.coin,
32+
source,
33+
type: 'independent',
34+
seed,
35+
});
36+
} catch (error: any) {
37+
res.status(error.status || 500).json({
38+
message: error.message || 'Failed to post key to KMS',
39+
});
40+
return;
41+
}
42+
}

src/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const defaultEnclavedConfig: EnclavedConfig = {
4949
bind: 'localhost',
5050
timeout: 305 * 1000,
5151
logFile: '',
52+
kmsUrl: '', // Will be overridden by environment variable
5253
tlsMode: TlsMode.ENABLED,
5354
mtlsRequestCert: false,
5455
mtlsRejectUnauthorized: false,
@@ -68,6 +69,12 @@ function determineTlsMode(): TlsMode {
6869
}
6970

7071
function enclavedEnvConfig(): Partial<EnclavedConfig> {
72+
const kmsUrl = readEnvVar('KMS_URL');
73+
74+
if (!kmsUrl) {
75+
throw new Error('KMS_URL environment variable is required and cannot be empty');
76+
}
77+
7178
return {
7279
appMode: AppMode.ENCLAVED,
7380
port: Number(readEnvVar('MASTER_BITGO_EXPRESS_PORT')),
@@ -80,6 +87,8 @@ function enclavedEnvConfig(): Partial<EnclavedConfig> {
8087
timeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_TIMEOUT')),
8188
keepAliveTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_KEEP_ALIVE_TIMEOUT')),
8289
headersTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_HEADERS_TIMEOUT')),
90+
// KMS settings
91+
kmsUrl,
8392
// TLS settings
8493
keyPath: readEnvVar('MASTER_BITGO_EXPRESS_KEYPATH'),
8594
crtPath: readEnvVar('MASTER_BITGO_EXPRESS_CRTPATH'),
@@ -112,6 +121,7 @@ function mergeEnclavedConfigs(...configs: Partial<EnclavedConfig>[]): EnclavedCo
112121
timeout: get('timeout'),
113122
keepAliveTimeout: get('keepAliveTimeout'),
114123
headersTimeout: get('headersTimeout'),
124+
kmsUrl: get('kmsUrl'),
115125
keyPath: get('keyPath'),
116126
crtPath: get('crtPath'),
117127
tlsKey: get('tlsKey'),

src/kms/kmsClient.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import debug from 'debug';
2+
import * as superagent from 'superagent';
3+
import { config, isMasterExpressConfig } from '../config';
4+
import { PostKeyKmsSchema, PostKeyParams, PostKeyResponse } from './types/postKey';
5+
6+
const debugLogger = debug('bitgo:express:kmsClient');
7+
8+
export class KmsClient {
9+
private readonly url: string;
10+
11+
constructor() {
12+
const cfg = config();
13+
if (isMasterExpressConfig(cfg)) {
14+
throw new Error('Configuration is not in enclaved express mode');
15+
}
16+
17+
if (!cfg.kmsUrl) {
18+
throw new Error('KMS URL not configured. Please set KMS_URL in your environment.');
19+
}
20+
21+
this.url = cfg.kmsUrl;
22+
debugLogger('kmsClient initialized with URL: %s', this.url);
23+
}
24+
25+
async postKey(params: PostKeyParams): Promise<PostKeyResponse> {
26+
debugLogger('Posting key to KMS: %O', params);
27+
28+
const kmsResponse = await superagent
29+
.post(`${this.url}/key`)
30+
.set('x-api-key', 'abc')
31+
.send(params);
32+
33+
try {
34+
PostKeyKmsSchema.parse(kmsResponse.body);
35+
} catch (error: any) {
36+
throw new Error(
37+
`KMS returned unexpected response${error.message ? `: ${error.message}` : ''}`,
38+
);
39+
}
40+
41+
const { pub, coin, source } = kmsResponse.body;
42+
return { pub, coin, source } as PostKeyResponse;
43+
}
44+
}

src/kms/types/postKey.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as z from 'zod';
2+
3+
export interface PostKeyParams {
4+
prv: string;
5+
pub: string;
6+
coin: string;
7+
source: string;
8+
type: 'independent' | 'enclaved';
9+
seed?: string; // Optional seed for key generation
10+
}
11+
12+
export interface PostKeyResponse {
13+
pub: string;
14+
coin: string;
15+
source: string;
16+
type: 'independent' | 'enclaved';
17+
}
18+
19+
export const PostKeyKmsSchema = z.object({
20+
pub: z.string(),
21+
coin: z.string(),
22+
source: z.enum(['independent', 'tss']),
23+
type: z.enum(['user', 'backup']),
24+
});

src/routes.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import express from 'express';
55
import debug from 'debug';
66
import pjson from '../package.json';
7+
import { BitGo, BitGoOptions } from 'bitgo';
8+
import { postIndependentKey } from './api/enclaved/postIndependentKey';
79

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

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

40-
function setupKeyGenRoutes() {
42+
function prepBitGo(req: express.Request, res: express.Response, next: express.NextFunction) {
43+
const bitgoConstructorParams: BitGoOptions = {};
44+
req.body.bitgo = new BitGo(bitgoConstructorParams);
45+
46+
next();
47+
}
48+
49+
function setupKeyGenRoutes(app: express.Application) {
4150
// Register additional routes here as needed
51+
app.post('/:coin/key/independentKey', prepBitGo, promiseWrapper(postIndependentKey));
4252
debugLogger('KeyGen routes configured');
4353
}
4454

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

5363
// Register keygen routes
54-
setupKeyGenRoutes();
64+
setupKeyGenRoutes(app);
5565

5666
// Add a catch-all for unsupported routes
5767
app.use('*', (_req, res) => {

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ interface BaseConfig {
3030
// Enclaved mode specific configuration
3131
export interface EnclavedConfig extends BaseConfig {
3232
appMode: AppMode.ENCLAVED;
33+
// KMS settings
34+
kmsUrl: string;
3335
// TLS settings
3436
keyPath?: string;
3537
crtPath?: string;

0 commit comments

Comments
 (0)