Skip to content

Commit

Permalink
[FABCN-412] TLS support for chaincode server (hyperledger#164)
Browse files Browse the repository at this point in the history
This patch adds TLS support for chaincode server.

To enable TLS, set tlsProps in the second argument for shim.server,
or add --chaincode-tls-cert-file and --chaincode-tls.key-file for CLI.

Client certificate validation can be enabled via tlsProps.clientCACerts
for shim.server or --chaincode-tls-client-cacert-file for CLI.

Also the -path options (for base64 encoded files) are supported.

Signed-off-by: Taku Shimosawa <[email protected]>
  • Loading branch information
shimos committed Jun 17, 2020
1 parent 6595d68 commit 3f06fd9
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 36 deletions.
20 changes: 13 additions & 7 deletions libraries/fabric-shim/lib/chaincode.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,21 +196,27 @@ class Shim {
return Logger.getLogger(name);
}

/**
* @interface ChaincodeServerTLSProperties
* @property {Buffer} key Private key for TLS
* @property {Buffer} cert Certificate for TLS
* @property {Buffer} [clientCACerts] CA certificate for client certificates if mutual TLS is used.
*/
/**
* @interface ChaincodeServerOpts
* @property {string} ccid Chaincode ID
* @property {string} address Listen address for the server
* @property {ChaincodeServerTLSProperties} [tlsProps] TLS properties if TLS is required.
*/
/**
* Returns a new Chaincode server. Should be called when the chaincode is launched in a server mode.
* @static
* @param {ChaincodeInterface} chaincode User-provided object that must implement <code>ChaincodeInterface</code>
* @param {ChaincodeSeverOpts} serverOpts Chaincode server options
* @param {ChaincodeServerOpts} serverOpts Chaincode server options
*/
static server(chaincode, serverOpts) {
return new ChaincodeServer(chaincode, serverOpts);
}
/**
* @typedef {Object} ChaincodeServerOpts
* @property {string} ccid Chaincode ID
* @property {string} address Listen address for the server
* @property {Object} tlsProps TLS properties. To be implemented. Should be null if TLS is not used.
*/
}

// special OID used by Fabric to save attributes in X.509 certificates
Expand Down
50 changes: 50 additions & 0 deletions libraries/fabric-shim/lib/cmds/serverCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

'use strict';

const fs = require('fs');

exports.command = 'server [options]';
exports.desc = 'Start the chaincode as a server';

Expand All @@ -19,6 +21,12 @@ const validOptions = {
'grpc.http2.max_pings_without_data': {type: 'number', default: 0},
'grpc.keepalive_permit_without_calls': {type: 'number', default: 1},
'chaincode-id': {type: 'string', required: true},
'chaincode-tls-cert-file': {type: 'string', conflicts: 'chaincode-tls-cert-path'},
'chaincode-tls-cert-path': {type: 'string', conflicts: 'chaincode-tls-cert-file'},
'chaincode-tls-key-file': {type: 'string', conflicts: 'chaincode-tls-key-path'},
'chaincode-tls-key-path': {type: 'string', conflicts: 'chaincode-tls-key-file'},
'chaincode-tls-client-cacert-file': {type: 'string', conflicts: 'chaincode-tls-client-cacert-path'},
'chaincode-tls-client-cacert-path': {type: 'string', conflicts: 'chaincode-tls-client-cacert-file'},
'module-path': {type: 'string', default: process.cwd()}
};

Expand All @@ -29,6 +37,20 @@ exports.builder = function (yargs) {

yargs.usage('fabric-chaincode-node server --chaincode-address 0.0.0.0:9999 --chaincode-id mycc_v0:abcdef12345678...');

yargs.check((argv) => {
if (argv['chaincode-tls-key-file'] || argv['chaincode-tls-key-path'] ||
argv['chaincode-tls-cert-file'] || argv['chaincode-tls-cert-path']) {
// TLS should be enabled
if (!argv['chaincode-tls-key-file'] && !argv['chaincode-tls-key-path']) {
throw new Error('A TLS option is set but no key is specified');
}
if (!argv['chaincode-tls-cert-file'] && !argv['chaincode-tls-cert-path']) {
throw new Error('A TLS option is set but no cert is specified');
}
}
return true;
});

return yargs;
};

Expand All @@ -45,6 +67,34 @@ exports.getArgs = function (yargs) {
argv[name] = yargs.argv[name];
}

// Load the cryptographic files if TLS is enabled
if (argv['chaincode-tls-key-file'] || argv['chaincode-tls-key-path'] ||
argv['chaincode-tls-cert-file'] || argv['chaincode-tls-cert-path']) {

const tlsProps = {};

if (argv['chaincode-tls-key-file']) {
tlsProps.key = fs.readFileSync(argv['chaincode-tls-key-file']);
} else {
tlsProps.key = Buffer.from(fs.readFileSync(argv['chaincode-tls-key-path']).toString(), 'base64');
}

if (argv['chaincode-tls-cert-file']) {
tlsProps.cert = fs.readFileSync(argv['chaincode-tls-cert-file']);
} else {
tlsProps.cert = Buffer.from(fs.readFileSync(argv['chaincode-tls-cert-path']).toString(), 'base64');
}

// If cacert option is specified, enable client certificate validation
if (argv['chaincode-tls-client-cacert-file']) {
tlsProps.clientCACerts = fs.readFileSync(argv['chaincode-tls-client-cacert-file']);
} else if (argv['chaincode-tls-client-cacert-path']) {
tlsProps.clientCACerts = Buffer.from(fs.readFileSync(argv['chaincode-tls-client-cacert-path']).toString(), 'base64');
}

argv.tlsProps = tlsProps;
}

// Translate the options to server options
argv.ccid = argv['chaincode-id'];
argv.address = argv['chaincode-address'];
Expand Down
31 changes: 27 additions & 4 deletions libraries/fabric-shim/lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,34 @@ class ChaincodeServer {
throw new Error('The "chaincode" argument must implement Init() and Invoke() methods');
}
if (typeof serverOpts.ccid !== 'string') {
throw new Error('Missing required property in severOpts: ccid');
throw new Error('Missing required property in serverOpts: ccid');
}
if (typeof serverOpts.address !== 'string') {
throw new Error('Missing required property in severOpts: address');
throw new Error('Missing required property in serverOpts: address');
}
if (typeof serverOpts.tlsProps === 'object' && serverOpts.tlsProps !== null) {
if (typeof serverOpts.tlsProps.key !== 'object' || serverOpts.tlsProps.key === null) {
throw new Error('Missing required property in serverOpts.tlsProps: key');
}
if (typeof serverOpts.tlsProps.cert !== 'object' || serverOpts.tlsProps.cert === null) {
throw new Error('Missing required property in serverOpts.tlsProps: cert');
}

let clientCACerts;
if (typeof serverOpts.tlsProps.clientCACerts === 'object' && serverOpts.tlsProps.clientCACerts !== null) {
clientCACerts = serverOpts.tlsProps.clientCACerts;
} else {
clientCACerts = null;
}

this._credentials = grpc.ServerCredentials.createSsl(clientCACerts, [
{
private_key: serverOpts.tlsProps.key,
cert_chain: serverOpts.tlsProps.cert
}
], clientCACerts === null ? false : true);
} else {
this._credentials = grpc.ServerCredentials.createInsecure();
}

// Create GRPC Server and register RPC handler
Expand All @@ -71,8 +95,7 @@ class ChaincodeServer {
return new Promise((resolve, reject) => {
logger.debug('ChaincodeServer trying to bind to ' + this._serverOpts.address);

// TODO: TLS Support
this._server.bindAsync(this._serverOpts.address, grpc.ServerCredentials.createInsecure(), (error, port) => {
this._server.bindAsync(this._serverOpts.address, this._credentials, (error, port) => {
if (!error) {
logger.debug('ChaincodeServer successfully bound to ' + port);

Expand Down
4 changes: 2 additions & 2 deletions libraries/fabric-shim/test/unit/chaincode.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const chaincodePath = '../../lib/chaincode.js';
const StartCommand = require('../../lib/cmds/startCommand.js');

const caPath = path.join(__dirname, 'test-ca.pem');
const certPath = path.join(__dirname, 'test-cert.pem');
const keyPath = path.join(__dirname, 'test-key.pem');
const certPath = path.join(__dirname, 'test-cert.base64');
const keyPath = path.join(__dirname, 'test-key.base64');

const ca = fs.readFileSync(caPath, 'utf8');
const key = fs.readFileSync(keyPath, 'utf8');
Expand Down
124 changes: 124 additions & 0 deletions libraries/fabric-shim/test/unit/cmds/serverCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const sinon = require('sinon');

const chai = require('chai');
const expect = chai.expect;
const fs = require('fs');
const path = require('path');

const yargs = require('yargs');
const Bootstrap = require('../../../lib/contract-spi/bootstrap');
Expand All @@ -30,6 +32,7 @@ describe('server cmd', () => {
it('should configure the builder function', () => {
sandbox.stub(yargs, 'options');
sandbox.stub(yargs, 'usage');
sandbox.stub(yargs, 'check');

chaincodeServerCommand.builder(yargs);

Expand All @@ -49,6 +52,7 @@ describe('server cmd', () => {
expect(args['module-path'].default).to.deep.equal(process.cwd());

expect(yargs.usage.calledOnce).to.be.true;
expect(yargs.check.calledOnce).to.be.true;
});
});

Expand All @@ -64,6 +68,16 @@ describe('server cmd', () => {
});

describe('.getArgs', () => {
const certFileEncoded = path.join(__dirname, '..', 'test-cert.base64');
const keyFileEncoded = path.join(__dirname, '..', 'test-key.base64');
const caFileEncoded = path.join(__dirname, '..', 'test-ca.base64');
const certFile = path.join(__dirname, '..', 'test-cert.pem');
const keyFile = path.join(__dirname, '..', 'test-key.pem');
const caFile = path.join(__dirname, '..', 'test-ca.pem');
const cert = Buffer.from(fs.readFileSync(certFileEncoded).toString(), 'base64');
const key = Buffer.from(fs.readFileSync(keyFileEncoded).toString(), 'base64');
const ca = Buffer.from(fs.readFileSync(caFileEncoded).toString(), 'base64');

it('should return the arguments properly', () => {
const argv = {
'chaincode-address': '0.0.0.0:9999',
Expand All @@ -83,5 +97,115 @@ describe('server cmd', () => {
expect(ret['chaincode-id']).to.be.undefined;
expect(ret['extra-options']).to.be.undefined;
});

it('should return the TLS arguments properly', () => {
const argv = {
'chaincode-address': '0.0.0.0:9999',
'chaincode-id': 'test_id:1',
'module-path': '/tmp/example',
'chaincode-tls-cert-path': certFileEncoded,
'chaincode-tls-key-path': keyFileEncoded
};

const ret = chaincodeServerCommand.getArgs({argv});

expect(ret.address).to.equal('0.0.0.0:9999');
expect(ret.ccid).to.equal('test_id:1');

expect(ret.tlsProps).to.deep.equal({
cert,
key
});
});

it('should return the mutual TLS arguments properly', () => {
const argv = {
'chaincode-address': '0.0.0.0:9999',
'chaincode-id': 'test_id:1',
'module-path': '/tmp/example',
'chaincode-tls-cert-path': certFileEncoded,
'chaincode-tls-key-path': keyFileEncoded,
'chaincode-tls-client-cacert-path': caFileEncoded
};

const ret = chaincodeServerCommand.getArgs({argv});

expect(ret.address).to.equal('0.0.0.0:9999');
expect(ret.ccid).to.equal('test_id:1');

expect(ret.tlsProps).to.deep.equal({
cert,
key,
clientCACerts: ca
});
});

it('should return the TLS arguments with PEM files properly', () => {
const argv = {
'chaincode-address': '0.0.0.0:9999',
'chaincode-id': 'test_id:1',
'module-path': '/tmp/example',
'chaincode-tls-cert-file': certFile,
'chaincode-tls-key-file': keyFile,
'chaincode-tls-client-cacert-file': caFile
};

const ret = chaincodeServerCommand.getArgs({argv});

expect(ret.address).to.equal('0.0.0.0:9999');
expect(ret.ccid).to.equal('test_id:1');

expect(ret.tlsProps).to.deep.equal({
cert,
key,
clientCACerts: ca
});
});
});

describe('parse arguments', () => {
it('should parse the arguments successfully', () => {
expect(() => {
chaincodeServerCommand.builder(yargs)
.exitProcess(false)
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999');
}).not.to.throw();
});

it('should parse the arguments successfully with TLS options', () => {
expect(() => {
chaincodeServerCommand.builder(yargs)
.exitProcess(false)
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' +
'--chaincode-tls-key-file tls.key --chaincode-tls-cert-file tls.pem');
}).not.to.throw();
});

it('should throw when conflicting arguments are passed', () => {
expect(() => {
chaincodeServerCommand.builder(yargs)
.exitProcess(false)
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' +
'--chaincode-tls-key-file tls.key --chaincode-tls-key-path tls.pem');
}).to.throw();
});

it('should throw when only TLS key is passed', () => {
expect(() => {
chaincodeServerCommand.builder(yargs)
.exitProcess(false)
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' +
'--chaincode-tls-key-file tls.key');
}).to.throw();
});

it('should throw when only TLS cert is passed', () => {
expect(() => {
chaincodeServerCommand.builder(yargs)
.exitProcess(false)
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' +
'--chaincode-tls-cert-file tls.pem');
}).to.throw();
});
});
});
4 changes: 2 additions & 2 deletions libraries/fabric-shim/test/unit/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ const mockChaincodeImpl = {
};

const ca = fs.readFileSync(path.join(__dirname, 'test-ca.pem'), 'utf8');
const key = fs.readFileSync(path.join(__dirname, 'test-key.pem'), 'utf8');
const cert = fs.readFileSync(path.join(__dirname, 'test-cert.pem'), 'utf8');
const key = fs.readFileSync(path.join(__dirname, 'test-key.base64'), 'utf8');
const cert = fs.readFileSync(path.join(__dirname, 'test-cert.base64'), 'utf8');

const mockOpts = {
pem: ca,
Expand Down
Loading

0 comments on commit 3f06fd9

Please sign in to comment.