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
3 changes: 2 additions & 1 deletion .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ jobs:
- name: Test
run: yarn test
env:
NODE_OPTIONS: "--max-old-space-size=4096"
MASTER_BITGO_EXPRESS_KEYPATH: ./test-ssl-key.pem
MASTER_BITGO_EXPRESS_CRTPATH: ./test-ssl-cert.pem
MTLS_ENABLED: true
MTLS_REQUEST_CERT: true
MTLS_REJECT_UNAUTHORIZED: false
MTLS_REJECT_UNAUTHORIZED: false
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist/
node_modules/
coverage/
coverage/
.idea/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
19 changes: 19 additions & 0 deletions enclaved-express-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUbE+vqSu9IgPoLJncJqX5aiXh2GkwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDUzMDIwMDgxNVoXDTI2MDUz
MDIwMDgxNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAsK1g8ts/QRdEHVnmiZSijvtKl08yf13JWY0yksJW0O6x
mrt2uotvONxMNKhGtS+hPjcJ2OC7fCyift8oaCDs7PfIXjVNcN2zRKPci8ihNWvQ
XrYGLTvL9EVHpH7CdlJU43BTaeFusH+k/qv2pW5WQnz13ULdq7yvnDFvJAeahm9X
ptvr9RX9f8Aki0Y82Zi04PCiaHdqBPPl1OfHi+brf4xl7pQUq7Pub94/IDywe+QK
lGFPQ0exSVm5X/7hWv/AxqEFa/Bqb6Uw0qatVqhrgLEHlLUYVXs9NDNXm+865+aT
kvW2dnBpTVRZjnXO+N+BwSj+PfI28RqMXsmIhraN4QIDAQABo1MwUTAdBgNVHQ4E
FgQUnsZxpWiuxqDq/1kV12rMos4NN/cwHwYDVR0jBBgwFoAUnsZxpWiuxqDq/1kV
12rMos4NN/cwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAsAX4
CCEsIVrKQKJKluEDqOFiuKg0SSe4xVqlSW9vy9z3UYLfOpw14EtB6Lzbtgw7z47w
AnZZS99Zzn3tbYd06/X+b3jThF5TU1gqBcYDCC9HCd9xpmQEC7Ss1Xa88ubjuh+U
E/7xN5xRt85S07VihJWscfY7JCUAELBo3gDCZLfgHjw8xMfPRceE36rkc5B2p60b
WEmmOBWjrSboMOfocasBTUVUMDvGgmxEGEmKgTYshr5lWKcIteisbZi7+OZlkflp
PUZNu5DUyQyjftr2EShndaceZrjgXt6ezoyQBVgPRA+N+NAJn+uBr3B3nZZb/mft
n3XsbtsAAoU49kEVOg==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions enclaved-express-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwrWDy2z9BF0Qd
WeaJlKKO+0qXTzJ/XclZjTKSwlbQ7rGau3a6i2843Ew0qEa1L6E+NwnY4Lt8LKJ+
3yhoIOzs98heNU1w3bNEo9yLyKE1a9BetgYtO8v0RUekfsJ2UlTjcFNp4W6wf6T+
q/alblZCfPXdQt2rvK+cMW8kB5qGb1em2+v1Ff1/wCSLRjzZmLTg8KJod2oE8+XU
58eL5ut/jGXulBSrs+5v3j8gPLB75AqUYU9DR7FJWblf/uFa/8DGoQVr8GpvpTDS
pq1WqGuAsQeUtRhVez00M1eb7zrn5pOS9bZ2cGlNVFmOdc7434HBKP498jbxGoxe
yYiGto3hAgMBAAECggEACrbTnJwBJQBf3WvN7Y/5n7Qg3ODXo3Ow5Iu0gm6N9z5S
akYYuCKr4bCHtTXIkUT3K/UIgCzOEdoJwf85zb7EFMbIpuCSoVfrKYF1EXZe7a9r
w81EE0rUs9aJDAeyi/Gy5iwHUvIcf/rtqugLcr194QBU+fsLwwC+oY6POonKLCwg
gXMxRJKx+tvp86x6s7FU8+vi40L/mGbCC1Bl1YqraVp8nT17ICivlcHVZESxCcKN
tCAY+xKK+zH+s5sHAQvM4OlGEvCeT1VISlw/VqkODxcGzMGUc+mbnGtWvDcYm9Pa
54F29QapkdUwBVnucpTICenaMrLr19H9l6Zgvfvx1QKBgQDnHHeGnSD1bpZUhIUt
2vIQpj7o26zsx472h9PmqIZwODpcYSfw8MknymXVnL78gdHqVDL0mgj59zemUpC7
CR9RJAlV7/3TghUPdDFQ/SGj9+xGG/L/HNyy6bQeLIZiGOlURcdFqAtKCIk+51oK
eTDCOuy3Ijrq6F6FbYnkXHmwcwKBgQDDtDdCo9EjnyiZ1qGOx5jLRNbGVlN77QFS
tSmegODAwfLQpm4c4fE2WnzeWlNXnzs8GRLSRASXIYunjQdvgpX1KTpffZPoSP3k
tvL8cbh1zk7X8cmvkrVJjcpn/ecWPgeHGV6MjuhxqhaVoEMjFsKRqQaSQ7r/gZsm
Vba82tuXWwKBgHFlSlBGcJF7/U7i5uWk8/ivWVav0p0rHT5hTttx/OS68ge5s/tI
aaqYaHbzPdJvcCvlvEq/+X+MiUWWZWUgCLmrUNlVs9k/jk3S2Q+/4+2sC8YqmIQM
CU3P1YyolBc12eZ7hlbrKP7eSVkP8uIIrJ/ggZ0psnboJNia8nmV1i95AoGAPNWE
Z/6sQDp1UHzbc5qv8F/Rs42aHeeqhZ8y9MZzFvgzFpDloazKYm72adgCGDazHxdc
NmhWVPRkiQzZxtv86VyLfKt4krg91B7aoYZoJJahA5dxblZYbCjbRkAy2UMm6+QC
9AZoUwzgQFq1A+9LRCQamtTbCBmttNjoGQSfRgkCgYEAi03ZXB+B0/4C2HRUz/GQ
6moLgB7FzC4MLY2KUDeiP+3zBPnfbGQM0OgPYu7OOWPC6lebS4C6DuPCTOSW1z8u
f4FeVSKGrofPx+DmEvUMsUQ5TvjRPwNL40PVlrdxytZi6nV01GveScPfljvQUd2r
DRaZg+YgV9Yl6wi8y2G5RD0=
-----END PRIVATE KEY-----
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
"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",
"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"
"superagent": "^8.0.9",
"proxy-agent": "6.4.0",
"proxyquire": "^2.1.3"
},
"devDependencies": {
"@types/body-parser": "^1.17.0",
Expand Down Expand Up @@ -54,6 +58,6 @@
"typescript": "^4.2.4"
},
"engines": {
"node": ">=14"
"node": ">=20.18.0"
}
Comment on lines 60 to 62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats the reason for not using 22, or 24?

}
2 changes: 1 addition & 1 deletion src/__tests__/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Routes', () => {

describe('Health Check Routes', () => {
it('should return 200 and status message for /ping', async () => {
const response = await request(app).get('/ping');
const response = await request(app).post('/ping');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('status', 'enclaved express server is ok!');
expect(response.body).toHaveProperty('timestamp');
Expand Down
107 changes: 107 additions & 0 deletions src/masterBitgoExpress/enclavedExpressClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as superagent from 'superagent';
import debug from 'debug';
import { config } from '../config';
import { isMasterExpressConfig } from '../types';
import https from 'https';

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

interface CreateIndependentKeychainParams {
source: 'user' | 'backup';
coin?: string;
type: 'independent';
seed?: string;
}

export interface IndependentKeychainResponse {
id: string;
pub: string;
encryptedPrv?: string;
type: 'independent';
source: 'user' | 'backup' | 'bitgo';
coin: string;
}

export class EnclavedExpressClient {
private readonly url: string;
private readonly sslCert: string;
private readonly coin?: string;
private readonly enableSSL: boolean;

constructor(coin?: string) {
const cfg = config();
if (!isMasterExpressConfig(cfg)) {
throw new Error('Configuration is not in master express mode');
}

if (!cfg.enclavedExpressUrl || !cfg.enclavedExpressSSLCert) {
throw new Error(
'Enclaved Express URL not configured. Please set BITGO_ENCLAVED_EXPRESS_URL and BITGO_ENCLAVED_EXPRESS_SSL_CERT in your environment.',
);
}

this.url = cfg.enclavedExpressUrl;
this.sslCert = cfg.enclavedExpressSSLCert;
this.coin = coin;
this.enableSSL = !!cfg.enableSSL;
debugLogger('EnclavedExpressClient initialized with URL: %s', this.url);
}

async ping(): Promise<void> {
try {
debugLogger('Pinging enclaved express at %s', this.url);
await superagent.get(`${this.url}/ping`).ca(this.sslCert).send();
} catch (error) {
const err = error as Error;
debugLogger('Failed to ping enclaved express: %s', err.message);
throw new Error(`Failed to ping enclaved express: ${err.message}`);
}
}

/**
* Create an independent multisig key for a given source and coin
*/
async createIndependentKeychain(
params: CreateIndependentKeychainParams,
): Promise<IndependentKeychainResponse> {
if (!this.coin) {
throw new Error('Coin not configured');
}
try {
debugLogger('Creating independent keychain for coin: %s', this.coin);
const { body: keychain } = await superagent
.post(`${this.url}/api/${this.coin}/key/independent`)
.ca(this.sslCert)
.agent(
new https.Agent({
rejectUnauthorized: this.enableSSL,
ca: this.sslCert,
}),
)
.type('json')
.send(params);
return keychain;
} catch (error) {
const err = error as Error;
debugLogger('Failed to create independent keychain: %s', err.message);
throw new Error(`Failed to create independent keychain: ${err.message}`);
}
}
}

/**
* Create an enclaved express client if the configuration is present
*/
export function createEnclavedExpressClient(coin?: string): EnclavedExpressClient | undefined {
try {
return new EnclavedExpressClient(coin);
} catch (error) {
const err = error as Error;
// If URL isn't configured, return undefined instead of throwing
if (err.message.includes('URL not configured')) {
debugLogger('Enclaved express URL not configured, returning undefined');
return undefined;
}
throw err;
}
}
128 changes: 128 additions & 0 deletions src/masterBitgoExpress/generateWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
GenerateWalletOptions,
promiseProps,
RequestTracer,
SupplementGenerateWalletOptions,
Keychain,
KeychainsTriplet,
Wallet,
WalletWithKeychains,
AddKeychainOptions,
} from '@bitgo/sdk-core';
import { createEnclavedExpressClient } from './enclavedExpressClient';
import _ from 'lodash';
import { BitGoRequest } from '../types/request';

/**
* This route is used to generate a multisig wallet when enclaved express is enabled
*/
export async function handleGenerateWalletOnPrem(req: BitGoRequest) {
const bitgo = req.bitgo;
const baseCoin = bitgo.coin(req.params.coin);

const enclavedExpressClient = createEnclavedExpressClient(req.params.coin);
if (!enclavedExpressClient) {
throw new Error(
'Enclaved express client not configured - enclaved express features will be disabled',
);
}

const params = req.body as GenerateWalletOptions;
const reqId = new RequestTracer();

// Assign the default multiSig type value based on the coin
if (!params.multisigType) {
params.multisigType = baseCoin.getDefaultMultisigType();
}

if (typeof params.label !== 'string') {
throw new Error('missing required string parameter label');
}

const { label, enterprise } = params;

// Create wallet parameters with type assertion to allow 'onprem' subtype
const walletParams = {
label: label,
m: 2,
n: 3,
keys: [],
type: 'cold',
subType: 'onprem',
multisigType: 'onchain',
} as unknown as SupplementGenerateWalletOptions; // TODO: Add onprem to the SDK subType and remove "unknown" type casting

if (!_.isUndefined(enterprise)) {
if (!_.isString(enterprise)) {
throw new Error('invalid enterprise argument, expecting string');
}
walletParams.enterprise = enterprise;
}

const userKeychainPromise = async (): Promise<Keychain> => {
const userKeychain = await enclavedExpressClient.createIndependentKeychain({
source: 'user',
coin: req.params.coin,
type: 'independent',
});
const userKeychainParams: AddKeychainOptions = {
pub: userKeychain.pub,
keyType: userKeychain.type,
source: userKeychain.source,
reqId,
};

const newUserKeychain = await baseCoin.keychains().add(userKeychainParams);
return _.extend({}, newUserKeychain, userKeychain);
};

const backupKeychainPromise = async (): Promise<Keychain> => {
const backupKeychain = await enclavedExpressClient.createIndependentKeychain({
source: 'backup',
coin: req.params.coin,
type: 'independent',
});
const backupKeychainParams: AddKeychainOptions = {
pub: backupKeychain.pub,
keyType: backupKeychain.type,
source: backupKeychain.source,
reqId,
};

const newBackupKeychain = await baseCoin.keychains().add(backupKeychainParams);
return _.extend({}, newBackupKeychain, backupKeychain);
};

const { userKeychain, backupKeychain, bitgoKeychain }: KeychainsTriplet = await promiseProps({
userKeychain: userKeychainPromise(),
backupKeychain: backupKeychainPromise(),
bitgoKeychain: baseCoin.keychains().createBitGo({
enterprise: params.enterprise,
reqId,
isDistributedCustody: params.isDistributedCustody,
}),
});

walletParams.keys = [userKeychain.id, backupKeychain.id, bitgoKeychain.id];

const keychains = {
userKeychain,
backupKeychain,
bitgoKeychain,
};

const finalWalletParams = await baseCoin.supplementGenerateWallet(walletParams, keychains);

bitgo.setRequestTracer(reqId);
const newWallet = await bitgo.post(baseCoin.url('/wallet/add')).send(finalWalletParams).result();

const result: WalletWithKeychains = {
wallet: new Wallet(bitgo, baseCoin, newWallet),
userKeychain: userKeychain,
backupKeychain: backupKeychain,
bitgoKeychain: bitgoKeychain,
responseType: 'WalletWithKeychains',
};

return { ...result, wallet: result.wallet.toJSON() };
}
Loading