Skip to content

feat(enclaved-express): setup enclaved express module #6122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
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
138 changes: 138 additions & 0 deletions modules/enclaved-express/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Enclaved Express

Enclaved Express is a secure signer implementation for cryptocurrency operations. It's designed to run in a secure enclave environment with flexible security options.

## Overview

This module provides a lightweight, dedicated signing server with these features:

- Focused on signing operations only - no BitGo API dependencies
- Optional TLS security for secure connections
- Client certificate validation when operating in mTLS mode
- Simple configuration and deployment

## Supported Operations

Currently, the following operations are supported:

- `/api/v2/:coin/sign` - Sign transactions
- `/api/v2/:coin/tssshare/:sharetype` - Generate TSS shares
- `/api/v2/ofc/signPayload` - Sign OFC payloads

## Configuration

Configuration is done via environment variables:

### Network Settings

- `PORT` - Port to listen on (default: 3080)
- `BIND` - Address to bind to (default: localhost)
- `TIMEOUT` - Request timeout in milliseconds (default: 305000)

### TLS Settings

- `TLS_ENABLED` - Enable/disable TLS (default: false)
- `TLS_CA_PATH` - Path to CA certificate file (optional, for client verification)
- `TLS_CERT_PATH` - Path to server certificate file (required when TLS is enabled)
- `TLS_KEY_PATH` - Path to server key file (required when TLS is enabled)
- `TLS_REQUEST_CERT` - Whether to request client certificates for mTLS (default: false)
- `TLS_REJECT_UNAUTHORIZED` - Whether to reject unauthorized connections (default: false)
- `TLS_ALLOWED_CLIENT_CERT_FINGERPRINTS` - Comma-separated list of allowed client certificate fingerprints (optional)

### Other Settings

- `LOGFILE` - Path to log file (optional)
- `DEBUG` - Debug namespaces to enable (e.g., 'enclaved:*')

## Running Enclaved Express

You can run enclaved-express just like any other Node.js service. From the BitGoJS project root:

```bash
cd modules/enclaved-express
npm install
npm run build
npm start
```

You can set any configuration environment variables as needed before running `npm start`.

### Example (HTTP only):

```bash
HTTP_ENABLED=true npm start
```

### Example (with TLS):

```bash
TLS_ENABLED=true \
TLS_CERT_PATH=/path/to/server.crt \
TLS_KEY_PATH=/path/to/server.key \
TLS_CA_PATH=/path/to/ca.crt \
npm start
```

---

If you wish to run enclaved-express in a container, you can use your own Docker or Podman setup, similar to how you would containerize any Node.js application. There is no longer a dedicated Dockerfile or docker-compose configuration in this module.

## Certificate Generation for mTLS

To generate certificates for mTLS authentication:

```bash
# Generate CA key and certificate
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 365 -subj "/CN=EnclaveCA"

# Generate server key and CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=enclaved-express"

# Sign server certificate with CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365

# Generate client key and CSR
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=client-service"

# Sign client certificate with CA
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365

# Calculate client certificate fingerprint for allowlist
openssl x509 -in client.crt -noout -fingerprint -sha256 | sed 's/://g' | awk -F= '{print $2}'
```

## Extending the Module

To add more endpoints to the enclaved-express module:

1. Update the `setupCustomRoutes` function in `src/routes.ts` to include new routes
2. Import and use handlers from the Express module or create new ones

Example of adding a new route:

```typescript
function setupCustomRoutes(app: express.Application) {
// Import handler from express module
import { handleNewOperation } from '../../express/src/clientRoutes';

// Add new route
app.post('/api/v2/custom/endpoint', promiseWrapper(handleNewOperation));

debug('Custom routes configured');
}
```

## Security Considerations

- Always keep private keys secure
- When using mTLS, use allowlisting for client certificates to ensure only approved services can connect
- Run in a production environment with proper firewall rules
- Consider using network policies to restrict which services can connect to the service
- Regularly rotate certificates and keys

## License

MIT
18 changes: 18 additions & 0 deletions modules/enclaved-express/bin/enclaved-express
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node

// TODO: Remove this unhandledRejection hook once BG-49996 is implemented.
process.on('unhandledRejection', (reason, promise) => {
console.error('----- Unhandled Rejection at -----');
console.error(promise);
console.error('----- Reason -----');
console.error(reason);
});

const { init } = require('../dist/src/enclavedApp');

if (require.main === module) {
init().catch((err) => {
console.log(`Fatal error: ${err.message}`);
console.log(err.stack);
});
}
61 changes: 61 additions & 0 deletions modules/enclaved-express/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "@bitgo/enclaved-express",
"version": "1.0.0",
"description": "BitGo Enclaved Express - Secure enclave for BitGo signing operations with mTLS",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"bin": {
"enclaved-express": "./bin/enclaved-express"
},
"scripts": {
"start": "node bin/enclaved-express",
"build": "yarn tsc --build --incremental --verbose .",
"test": "jest",
"lint": "eslint --quiet .",
"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/abstract-lightning": "^4.2.3",
"@bitgo/sdk-core": "^32.2.0",
"@bitgo/utxo-lib": "^11.3.0",
"@types/proxyquire": "^1.3.31",
"argparse": "^1.0.10",
"bitgo": "^43.2.0",
"body-parser": "^1.20.3",
"connect-timeout": "^1.9.0",
"debug": "^3.1.0",
"dotenv": "^16.0.0",
"express": "4.17.3",
"io-ts": "npm:@bitgo-forks/[email protected]",
"lodash": "^4.17.20",
"morgan": "^1.9.1",
"proxy-agent": "6.4.0",
"proxyquire": "^2.1.3",
"superagent": "^9.0.1"
},
"devDependencies": {
"@bitgo/public-types": "4.17.0",
"@bitgo/sdk-lib-mpc": "^10.2.0",
"@bitgo/sdk-test": "^8.0.81",
"@types/argparse": "^1.0.36",
"@types/body-parser": "^1.17.0",
"@types/express": "4.17.13",
"@types/lodash": "^4.14.121",
"@types/morgan": "^1.7.35",
"@types/node": "^16.18.46",
"@types/sinon": "^10.0.11",
"@types/supertest": "^2.0.11",
"nock": "^13.3.1",
"nyc": "^15.0.0",
"should": "^13.2.3",
"should-http": "^0.1.1",
"should-sinon": "^0.0.6",
"sinon": "^13.0.1",
"supertest": "^4.0.2",
"supertest-as-promised": "1.0.0",
"typescript": "^4.2.4"
},
"engines": {
"node": ">=14"
}
}
96 changes: 96 additions & 0 deletions modules/enclaved-express/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @prettier
*/

export enum TlsMode {
DISABLED = 'disabled', // No TLS (plain HTTP)
ENABLED = 'enabled', // TLS with server cert only
MTLS = 'mtls' // TLS with both server and client certs
}

export interface Config {
port: number;
bind: string;
ipc?: string;
debugNamespace?: string[];
// TLS settings
keyPath?: string;
crtPath?: string;
tlsKey?: string;
tlsCert?: string;
tlsMode: TlsMode;
// mTLS settings
mtlsRequestCert?: boolean;
mtlsRejectUnauthorized?: boolean;
mtlsAllowedClientFingerprints?: string[];
// Other settings
logFile?: string;
timeout: number;
keepAliveTimeout?: number;
headersTimeout?: number;
}

const defaultConfig: Config = {
port: 3080,
bind: 'localhost',
timeout: 305 * 1000,
logFile: '',
tlsMode: TlsMode.ENABLED, // Default to TLS enabled
mtlsRequestCert: false,
mtlsRejectUnauthorized: false,
};

function readEnvVar(name: string): string | undefined {
if (process.env[name] !== undefined && process.env[name] !== '') {
return process.env[name];
}
}

export function config(): Config {
const envConfig: Partial<Config> = {
port: Number(readEnvVar('MASTER_BITGO_EXPRESS_PORT')) || defaultConfig.port,
bind: readEnvVar('MASTER_BITGO_EXPRESS_BIND') || defaultConfig.bind,
ipc: readEnvVar('MASTER_BITGO_EXPRESS_IPC'),
debugNamespace: (readEnvVar('MASTER_BITGO_EXPRESS_DEBUG_NAMESPACE') || '').split(',').filter(Boolean),
// Basic TLS settings from MASTER_BITGO_EXPRESS
keyPath: readEnvVar('MASTER_BITGO_EXPRESS_KEYPATH'),
crtPath: readEnvVar('MASTER_BITGO_EXPRESS_CRTPATH'),
tlsKey: readEnvVar('MASTER_BITGO_EXPRESS_TLS_KEY'),
tlsCert: readEnvVar('MASTER_BITGO_EXPRESS_TLS_CERT'),
// Determine TLS mode
tlsMode: readEnvVar('MASTER_BITGO_EXPRESS_DISABLE_TLS') === 'true'
? TlsMode.DISABLED
: readEnvVar('MTLS_ENABLED') === 'true'
? TlsMode.MTLS
: TlsMode.ENABLED,
// mTLS settings
mtlsRequestCert: readEnvVar('MTLS_REQUEST_CERT') === 'true',
mtlsRejectUnauthorized: readEnvVar('MTLS_REJECT_UNAUTHORIZED') === 'true',
mtlsAllowedClientFingerprints: readEnvVar('MTLS_ALLOWED_CLIENT_FINGERPRINTS')?.split(','),
// Other settings
logFile: readEnvVar('MASTER_BITGO_EXPRESS_LOGFILE'),
timeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_TIMEOUT')) || defaultConfig.timeout,
keepAliveTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_KEEP_ALIVE_TIMEOUT')),
headersTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_HEADERS_TIMEOUT')),
};

// Support loading key/cert from file if keyPath/crtPath are set and tlsKey/tlsCert are not
if (!envConfig.tlsKey && envConfig.keyPath) {
try {
envConfig.tlsKey = require('fs').readFileSync(envConfig.keyPath, 'utf-8');
} catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
throw new Error(`Failed to read TLS key from keyPath: ${err.message}`);
}
}
if (!envConfig.tlsCert && envConfig.crtPath) {
try {
envConfig.tlsCert = require('fs').readFileSync(envConfig.crtPath, 'utf-8');
} catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
throw new Error(`Failed to read TLS certificate from crtPath: ${err.message}`);
}
}

return { ...defaultConfig, ...envConfig };
}
Loading
Loading