Skip to content

Commit

Permalink
feat(grpc): add SSL to gRPC
Browse files Browse the repository at this point in the history
This commit adds SSL encryption to the gRPC server. If the needed
certificate doesn't exist a new one will be generated and stored in the
data directory of XUD.
  • Loading branch information
michael1011 committed Oct 1, 2018
1 parent b0e4f3a commit 50551f5
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .env
@@ -1 +1 @@
GRPC_SSL_CIPHER_SUITES='HIGH+ECDSA'
GRPC_SSL_CIPHER_SUITES='HIGH+ECDSA:ECDHE-RSA-AES128-GCM-SHA256'
14 changes: 12 additions & 2 deletions bin/xucli
@@ -1,22 +1,32 @@
#!/usr/bin/env node
const dotenv = require('dotenv');

dotenv.config({
path: '../.env',
});

require('yargs')
.options({
rpc: {
hidden: true,
},
'rpc.port': {
rpcport: {
alias: 'p',
default: 8886,
describe: 'RPC service port',
type: 'number',
},
'rpc.host': {
rpchost: {
alias: 'h',
default: 'localhost',
describe: 'RPC service hostname',
type: 'string',
},
tlscertpath: {
alias: 'c',
describe: 'Path to the TLS certificate of xud',
type: 'string',
},
})
.commandDir('../dist/cli/commands/', { recurse: true })
.demandCommand(1, '')
Expand Down
2 changes: 1 addition & 1 deletion lib/Config.ts
Expand Up @@ -2,7 +2,7 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import toml from 'toml';
import { deepMerge, isPlainObject } from './utils/utils';
import { deepMerge } from './utils/utils';
import { PoolConfig } from './p2p/Pool';
import { LndClientConfig } from './lndclient/LndClient';
import { RaidenClientConfig } from './raidenclient/RaidenClient';
Expand Down
16 changes: 14 additions & 2 deletions lib/Xud.ts
@@ -1,3 +1,4 @@
import path from 'path';
import bootstrap from './bootstrap';
import Logger from './Logger';
import Config from './Config';
Expand Down Expand Up @@ -121,7 +122,13 @@ class Xud extends EventEmitter {
// start rpc server last
if (!this.config.rpc.disable) {
this.rpcServer = new GrpcServer(loggers.rpc, this.service);
const listening = this.rpcServer.listen(this.config.rpc.port, this.config.rpc.host);
const listening = this.rpcServer.listen(
this.config.rpc.port,
this.config.rpc.host,
path.join(this.config.xudir, 'tls.cert'),
path.join(this.config.xudir, 'tls.key'),
);

if (!listening) {
// if rpc should be enabled but fails to start, treat it as a fatal error
this.logger.error('Could not start gRPC server, exiting...');
Expand All @@ -132,7 +139,12 @@ class Xud extends EventEmitter {
if (!this.config.webproxy.disable) {
this.grpcAPIProxy = new GrpcWebProxyServer(loggers.rpc);
try {
await this.grpcAPIProxy.listen(this.config.webproxy.port, this.config.rpc.port, this.config.rpc.host);
await this.grpcAPIProxy.listen(
this.config.webproxy.port,
this.config.rpc.port,
this.config.rpc.host,
path.join(this.config.xudir, 'tls.cert'),
);
} catch (err) {
this.logger.error('Could not start gRPC web proxy server', err);
}
Expand Down
28 changes: 25 additions & 3 deletions lib/cli/command.ts
@@ -1,11 +1,33 @@
import { Arguments } from 'yargs';
import fs from 'fs';
import os from 'os';
import path from 'path';
import grpc from 'grpc';
import { XudClient } from '../proto/xudrpc_grpc_pb';

export const loadXudClient = (argv: Arguments) => {
// TODO load saved cert from disk
const credentials = grpc.credentials.createInsecure();
return new XudClient(`${argv.rpc.host}:${argv.rpc.port}`, credentials);
const getXudDir = () => {
switch (os.platform()) {
case 'win32': {
const homeDir = process.env.LOCALAPPDATA!;
return path.join(homeDir, 'Xud');
}
case 'darwin': {
const homeDir = process.env.HOME!;
return path.join(homeDir, '.xud');
}
default: {
const homeDir = process.env.HOME!;
return path.join(homeDir, '.xud');
}
}
};

const certPath = argv.tlscertpath ? argv.tlscertpath : path.join(getXudDir(), 'tls.cert');
const cert = fs.readFileSync(certPath);
const credentials = grpc.credentials.createSsl(cert);

return new XudClient(`${argv.rpchost}:${argv.rpcport}`, credentials);
};

interface grpcResponse {
Expand Down
2 changes: 1 addition & 1 deletion lib/cli/utils.ts
@@ -1,5 +1,5 @@
import { callback, loadXudClient } from './command';
import { Arguments, Argv } from 'yargs';
import { callback, loadXudClient } from './command';
import { PlaceOrderRequest, OrderSide } from '../proto/xudrpc_pb';

export const orderBuilder = (argv: Argv, command: string) => argv
Expand Down
89 changes: 87 additions & 2 deletions lib/grpc/GrpcServer.ts
@@ -1,4 +1,7 @@
import fs from 'fs';
import { hostname } from 'os';
import grpc, { Server } from 'grpc';
import { pki, md } from 'node-forge';
import assert from 'assert';
import Logger from '../Logger';
import GrpcService from './GrpcService';
Expand Down Expand Up @@ -44,10 +47,31 @@ class GrpcServer {
* Start the server and begin listening on the provided port
* @returns true if the server started listening successfully, false otherwise
*/
public listen = (port: number, host: string) => {
public listen = async (port: number, host: string, tlsCertPath: string, tlsKeyPath: string): Promise<boolean> => {
assert(Number.isInteger(port) && port > 1023 && port < 65536, 'port must be an integer between 1024 and 65535');

const bindCode = this.server.bind(`${host}:${port}`, grpc.ServerCredentials.createInsecure());
let certificate: Buffer;
let privateKey: Buffer;

if (!fs.existsSync(tlsCertPath) || !fs.existsSync(tlsKeyPath)) {
this.logger.debug('Could not find gRPC TLS certificate. Generating new one');
const { tlsCert, tlsKey } = this.generateCertificate(tlsCertPath, tlsKeyPath);

certificate = Buffer.from(tlsCert);
privateKey = Buffer.from(tlsKey);
} else {
certificate = fs.readFileSync(tlsCertPath);
privateKey = fs.readFileSync(tlsKeyPath);
}

// tslint:disable-next-line:no-null-keyword
const credentials = grpc.ServerCredentials.createSsl(null,
[{
cert_chain: certificate,
private_key: privateKey,
}], false);

const bindCode = this.server.bind(`${host}:${port}`, credentials);
if (bindCode !== port) {
const error = errors.COULD_NOT_BIND(port.toString());
this.logger.error(error.message);
Expand All @@ -70,6 +94,67 @@ class GrpcServer {
});
});
}

/**
* Generate a new certificate and save it to the disk
* @returns the cerificate and its private key
*/
private generateCertificate = (tlsCertPath: string, tlsKeyPath: string): { tlsCert: string, tlsKey: string } => {
const keys = pki.rsa.generateKeyPair(1024);
const cert = pki.createCertificate();

cert.publicKey = keys.publicKey;
cert.serialNumber = String(Math.floor(Math.random() * 1024) + 1);

// TODO: handle expired certificates
const date = new Date();
cert.validity.notBefore = date;
cert.validity.notAfter = new Date(date.getFullYear() + 5, date.getMonth(), date.getDay());

const attributes = [
{
name: 'organizationName',
value: 'XUD autogenerated certificate',
},
{
name: 'commonName',
value: hostname(),
},
];

cert.setSubject(attributes);
cert.setIssuer(attributes);

// TODO: add tlsextradomain and tlsextraip options
cert.setExtensions([
{
name: 'subjectAltName',
altNames: [
{
type: 2,
value: 'localhost',
},
{
type: 7,
ip: '127.0.0.1',
},
],
},
]);

cert.sign(keys.privateKey, md.sha256.create());

const certificate = pki.certificateToPem(cert);
const privateKey = pki.privateKeyToPem(keys.privateKey);

fs.writeFileSync(tlsCertPath, certificate);
fs.writeFileSync(tlsKeyPath, privateKey);

return {
tlsCert: certificate,
tlsKey: privateKey,
};
}
}

export default GrpcServer;
11 changes: 9 additions & 2 deletions lib/grpc/webproxy/GrpcWebProxyServer.ts
Expand Up @@ -6,6 +6,8 @@ import grpcGateway from '@exchangeunion/grpc-dynamic-gateway';
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import grpc from 'grpc';
import fs from 'fs';

const swaggerDocument = require('../../proto/xudrpc.swagger.json');

/** A class representing an HTTP web proxy for the gRPC service. */
Expand All @@ -23,10 +25,15 @@ class GrpcWebProxyServer {
/**
* Start the server and begins listening on the specified proxy port.
*/
public listen = (proxyPort: number, grpcPort: number, grpcHost: string): Promise<void> => {
public listen = (proxyPort: number, grpcPort: number, grpcHost: string, tlsCertPath: string): Promise<void> => {
// Load the proxy on / URL
const protoPath = path.join(__dirname, '..', '..', '..', 'proto');
const gateway = grpcGateway(['xudrpc.proto'], `${grpcHost}:${grpcPort}`, grpc.credentials.createInsecure(), protoPath);
const gateway = grpcGateway(
['xudrpc.proto'],
`${grpcHost}:${grpcPort}`,
grpc.credentials.createSsl(fs.readFileSync(tlsCertPath)),
protoPath,
);
this.app.use('/api/', gateway);
return new Promise((resolve, reject) => {
/** A handler to handle an error while trying to begin listening. */
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -121,6 +121,7 @@
"google-protobuf": "^3.5.0",
"grpc": "^1.13.1",
"gulp": "^4.0.0",
"node-forge": "^0.7.6",
"secp256k1": "^3.5.0",
"sequelize": "^4.37.3",
"sqlite3": "^4.0.2",
Expand All @@ -141,6 +142,7 @@
"@types/express": "^4.16.0",
"@types/gulp": "^4.0.5",
"@types/mocha": "^5.2.0",
"@types/node-forge": "^0.7.5",
"@types/secp256k1": "^3.5.0",
"@types/sequelize": "^4.27.20",
"@types/swagger-ui-express": "^3.0.0",
Expand Down

0 comments on commit 50551f5

Please sign in to comment.