Skip to content

Commit 7b3696f

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 0d97d24 commit 7b3696f

File tree

8 files changed

+9457
-113
lines changed

8 files changed

+9457
-113
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
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": "^46.0.1",
2021
"body-parser": "^1.20.3",
2122
"connect-timeout": "^1.9.0",
2223
"debug": "^3.1.0",
@@ -54,6 +55,6 @@
5455
"typescript": "^4.2.4"
5556
},
5657
"engines": {
57-
"node": ">=14"
58+
"node": ">=14 <21"
5859
}
5960
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { BitGo, KeyPair } from 'bitgo'
2+
import * as express from 'express'
3+
import { KmsClient } from '../../kms/kmsClient';
4+
5+
export async function postIndependentKey(req: express.Request): Promise<any> {
6+
7+
const { source, _seed } = req.body; // TODO: typing and seed handling
8+
if (!source) {
9+
throw new Error('Source is required for key generation');
10+
}
11+
12+
// setup clients
13+
const bitgo: BitGo = req.body.bitgo;
14+
const kms = new KmsClient();
15+
16+
// create public and private key pairs on BitGo SDK
17+
const coin = bitgo.coin(req.params.coin);
18+
const { pub, prv } = coin.keychains().create();
19+
20+
if (!pub) {
21+
throw new Error('BitGo SDK failed to create public key');
22+
}
23+
24+
// post key to KMS for encryption and storage
25+
return await kms.postKey({
26+
pub,
27+
prv,
28+
coin: req.params.coin,
29+
source,
30+
type: 'independent',
31+
});
32+
}

src/config.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function determineAppMode(): AppMode {
4343
// ENCLAVED MODE CONFIGURATION
4444
// ============================================================================
4545

46-
const defaultEnclavedConfig: EnclavedConfig = {
46+
const defaultEnclavedConfig: Partial<EnclavedConfig> = {
4747
appMode: AppMode.ENCLAVED,
4848
port: 3080,
4949
bind: 'localhost',
@@ -68,6 +68,12 @@ function determineTlsMode(): TlsMode {
6868
}
6969

7070
function enclavedEnvConfig(): Partial<EnclavedConfig> {
71+
const kmsUrl = readEnvVar('KMS_URL');
72+
73+
if (!kmsUrl) {
74+
throw new Error('KMS_URL environment variable is required and cannot be empty');
75+
}
76+
7177
return {
7278
appMode: AppMode.ENCLAVED,
7379
port: Number(readEnvVar('MASTER_BITGO_EXPRESS_PORT')),
@@ -80,6 +86,8 @@ function enclavedEnvConfig(): Partial<EnclavedConfig> {
8086
timeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_TIMEOUT')),
8187
keepAliveTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_KEEP_ALIVE_TIMEOUT')),
8288
headersTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_HEADERS_TIMEOUT')),
89+
// KMS settings
90+
kmsUrl,
8391
// TLS settings
8492
keyPath: readEnvVar('MASTER_BITGO_EXPRESS_KEYPATH'),
8593
crtPath: readEnvVar('MASTER_BITGO_EXPRESS_CRTPATH'),
@@ -95,7 +103,7 @@ function enclavedEnvConfig(): Partial<EnclavedConfig> {
95103

96104
function mergeEnclavedConfigs(...configs: Partial<EnclavedConfig>[]): EnclavedConfig {
97105
function get<T extends keyof EnclavedConfig>(k: T): EnclavedConfig[T] {
98-
return configs.reduce(
106+
return (configs as unknown as any[]).reduce(
99107
(entry: EnclavedConfig[T], config) =>
100108
!isNilOrNaN(config[k]) ? (config[k] as EnclavedConfig[T]) : entry,
101109
defaultEnclavedConfig[k],
@@ -112,6 +120,7 @@ function mergeEnclavedConfigs(...configs: Partial<EnclavedConfig>[]): EnclavedCo
112120
timeout: get('timeout'),
113121
keepAliveTimeout: get('keepAliveTimeout'),
114122
headersTimeout: get('headersTimeout'),
123+
kmsUrl: get('kmsUrl'),
115124
keyPath: get('keyPath'),
116125
crtPath: get('crtPath'),
117126
tlsKey: get('tlsKey'),

src/kms/kmsClient.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import debug from "debug";
2+
import * as superagent from "superagent";
3+
import { config, isMasterExpressConfig } from "../config";
4+
import { 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+
try {
29+
const kmsResponse = await superagent.post(`${this.url}/key`)
30+
.set("x-api-key", "abc")
31+
.send(params);
32+
33+
const { pub, coin, source, type } = kmsResponse.body;
34+
return { pub, coin, source } as PostKeyResponse;
35+
} catch (error) { // TODO: better error handling
36+
const err = error as Error;
37+
debugLogger('Error posting key to KMS: %O', err);
38+
throw new Error(`Failed to post key to KMS: ${err.message}`);
39+
}
40+
}
41+
}

src/kms/types/postKey.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export interface PostKeyParams {
2+
prv: string;
3+
pub: string;
4+
coin: string;
5+
source: string;
6+
type: 'independent' | 'enclaved';
7+
seed?: string; // Optional seed for key generation
8+
}
9+
10+
export interface PostKeyResponse {
11+
pub: string;
12+
coin: string;
13+
source: string;
14+
type: 'independent' | 'enclaved';
15+
}

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/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)