Skip to content

Commit 255325d

Browse files
authored
[FABCN-412] TLS support for chaincode server (#164)
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 <taku.shimosawa@hal.hitachi.com>
1 parent 38a7123 commit 255325d

12 files changed

Lines changed: 340 additions & 36 deletions

File tree

libraries/fabric-shim/lib/chaincode.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,21 +196,27 @@ class Shim {
196196
return Logger.getLogger(name);
197197
}
198198

199+
/**
200+
* @interface ChaincodeServerTLSProperties
201+
* @property {Buffer} key Private key for TLS
202+
* @property {Buffer} cert Certificate for TLS
203+
* @property {Buffer} [clientCACerts] CA certificate for client certificates if mutual TLS is used.
204+
*/
205+
/**
206+
* @interface ChaincodeServerOpts
207+
* @property {string} ccid Chaincode ID
208+
* @property {string} address Listen address for the server
209+
* @property {ChaincodeServerTLSProperties} [tlsProps] TLS properties if TLS is required.
210+
*/
199211
/**
200212
* Returns a new Chaincode server. Should be called when the chaincode is launched in a server mode.
201213
* @static
202214
* @param {ChaincodeInterface} chaincode User-provided object that must implement <code>ChaincodeInterface</code>
203-
* @param {ChaincodeSeverOpts} serverOpts Chaincode server options
215+
* @param {ChaincodeServerOpts} serverOpts Chaincode server options
204216
*/
205217
static server(chaincode, serverOpts) {
206218
return new ChaincodeServer(chaincode, serverOpts);
207219
}
208-
/**
209-
* @typedef {Object} ChaincodeServerOpts
210-
* @property {string} ccid Chaincode ID
211-
* @property {string} address Listen address for the server
212-
* @property {Object} tlsProps TLS properties. To be implemented. Should be null if TLS is not used.
213-
*/
214220
}
215221

216222
// special OID used by Fabric to save attributes in X.509 certificates

libraries/fabric-shim/lib/cmds/serverCommand.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
'use strict';
88

9+
const fs = require('fs');
10+
911
exports.command = 'server [options]';
1012
exports.desc = 'Start the chaincode as a server';
1113

@@ -19,6 +21,12 @@ const validOptions = {
1921
'grpc.http2.max_pings_without_data': {type: 'number', default: 0},
2022
'grpc.keepalive_permit_without_calls': {type: 'number', default: 1},
2123
'chaincode-id': {type: 'string', required: true},
24+
'chaincode-tls-cert-file': {type: 'string', conflicts: 'chaincode-tls-cert-path'},
25+
'chaincode-tls-cert-path': {type: 'string', conflicts: 'chaincode-tls-cert-file'},
26+
'chaincode-tls-key-file': {type: 'string', conflicts: 'chaincode-tls-key-path'},
27+
'chaincode-tls-key-path': {type: 'string', conflicts: 'chaincode-tls-key-file'},
28+
'chaincode-tls-client-cacert-file': {type: 'string', conflicts: 'chaincode-tls-client-cacert-path'},
29+
'chaincode-tls-client-cacert-path': {type: 'string', conflicts: 'chaincode-tls-client-cacert-file'},
2230
'module-path': {type: 'string', default: process.cwd()}
2331
};
2432

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

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

40+
yargs.check((argv) => {
41+
if (argv['chaincode-tls-key-file'] || argv['chaincode-tls-key-path'] ||
42+
argv['chaincode-tls-cert-file'] || argv['chaincode-tls-cert-path']) {
43+
// TLS should be enabled
44+
if (!argv['chaincode-tls-key-file'] && !argv['chaincode-tls-key-path']) {
45+
throw new Error('A TLS option is set but no key is specified');
46+
}
47+
if (!argv['chaincode-tls-cert-file'] && !argv['chaincode-tls-cert-path']) {
48+
throw new Error('A TLS option is set but no cert is specified');
49+
}
50+
}
51+
return true;
52+
});
53+
3254
return yargs;
3355
};
3456

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

70+
// Load the cryptographic files if TLS is enabled
71+
if (argv['chaincode-tls-key-file'] || argv['chaincode-tls-key-path'] ||
72+
argv['chaincode-tls-cert-file'] || argv['chaincode-tls-cert-path']) {
73+
74+
const tlsProps = {};
75+
76+
if (argv['chaincode-tls-key-file']) {
77+
tlsProps.key = fs.readFileSync(argv['chaincode-tls-key-file']);
78+
} else {
79+
tlsProps.key = Buffer.from(fs.readFileSync(argv['chaincode-tls-key-path']).toString(), 'base64');
80+
}
81+
82+
if (argv['chaincode-tls-cert-file']) {
83+
tlsProps.cert = fs.readFileSync(argv['chaincode-tls-cert-file']);
84+
} else {
85+
tlsProps.cert = Buffer.from(fs.readFileSync(argv['chaincode-tls-cert-path']).toString(), 'base64');
86+
}
87+
88+
// If cacert option is specified, enable client certificate validation
89+
if (argv['chaincode-tls-client-cacert-file']) {
90+
tlsProps.clientCACerts = fs.readFileSync(argv['chaincode-tls-client-cacert-file']);
91+
} else if (argv['chaincode-tls-client-cacert-path']) {
92+
tlsProps.clientCACerts = Buffer.from(fs.readFileSync(argv['chaincode-tls-client-cacert-path']).toString(), 'base64');
93+
}
94+
95+
argv.tlsProps = tlsProps;
96+
}
97+
4898
// Translate the options to server options
4999
argv.ccid = argv['chaincode-id'];
50100
argv.address = argv['chaincode-address'];

libraries/fabric-shim/lib/server.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,34 @@ class ChaincodeServer {
4747
throw new Error('The "chaincode" argument must implement Init() and Invoke() methods');
4848
}
4949
if (typeof serverOpts.ccid !== 'string') {
50-
throw new Error('Missing required property in severOpts: ccid');
50+
throw new Error('Missing required property in serverOpts: ccid');
5151
}
5252
if (typeof serverOpts.address !== 'string') {
53-
throw new Error('Missing required property in severOpts: address');
53+
throw new Error('Missing required property in serverOpts: address');
54+
}
55+
if (typeof serverOpts.tlsProps === 'object' && serverOpts.tlsProps !== null) {
56+
if (typeof serverOpts.tlsProps.key !== 'object' || serverOpts.tlsProps.key === null) {
57+
throw new Error('Missing required property in serverOpts.tlsProps: key');
58+
}
59+
if (typeof serverOpts.tlsProps.cert !== 'object' || serverOpts.tlsProps.cert === null) {
60+
throw new Error('Missing required property in serverOpts.tlsProps: cert');
61+
}
62+
63+
let clientCACerts;
64+
if (typeof serverOpts.tlsProps.clientCACerts === 'object' && serverOpts.tlsProps.clientCACerts !== null) {
65+
clientCACerts = serverOpts.tlsProps.clientCACerts;
66+
} else {
67+
clientCACerts = null;
68+
}
69+
70+
this._credentials = grpc.ServerCredentials.createSsl(clientCACerts, [
71+
{
72+
private_key: serverOpts.tlsProps.key,
73+
cert_chain: serverOpts.tlsProps.cert
74+
}
75+
], clientCACerts === null ? false : true);
76+
} else {
77+
this._credentials = grpc.ServerCredentials.createInsecure();
5478
}
5579

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

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

libraries/fabric-shim/test/unit/chaincode.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const chaincodePath = '../../lib/chaincode.js';
2121
const StartCommand = require('../../lib/cmds/startCommand.js');
2222

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

2727
const ca = fs.readFileSync(caPath, 'utf8');
2828
const key = fs.readFileSync(keyPath, 'utf8');

libraries/fabric-shim/test/unit/cmds/serverCommand.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const sinon = require('sinon');
1010

1111
const chai = require('chai');
1212
const expect = chai.expect;
13+
const fs = require('fs');
14+
const path = require('path');
1315

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

3437
chaincodeServerCommand.builder(yargs);
3538

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

5154
expect(yargs.usage.calledOnce).to.be.true;
55+
expect(yargs.check.calledOnce).to.be.true;
5256
});
5357
});
5458

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

6670
describe('.getArgs', () => {
71+
const certFileEncoded = path.join(__dirname, '..', 'test-cert.base64');
72+
const keyFileEncoded = path.join(__dirname, '..', 'test-key.base64');
73+
const caFileEncoded = path.join(__dirname, '..', 'test-ca.base64');
74+
const certFile = path.join(__dirname, '..', 'test-cert.pem');
75+
const keyFile = path.join(__dirname, '..', 'test-key.pem');
76+
const caFile = path.join(__dirname, '..', 'test-ca.pem');
77+
const cert = Buffer.from(fs.readFileSync(certFileEncoded).toString(), 'base64');
78+
const key = Buffer.from(fs.readFileSync(keyFileEncoded).toString(), 'base64');
79+
const ca = Buffer.from(fs.readFileSync(caFileEncoded).toString(), 'base64');
80+
6781
it('should return the arguments properly', () => {
6882
const argv = {
6983
'chaincode-address': '0.0.0.0:9999',
@@ -83,5 +97,115 @@ describe('server cmd', () => {
8397
expect(ret['chaincode-id']).to.be.undefined;
8498
expect(ret['extra-options']).to.be.undefined;
8599
});
100+
101+
it('should return the TLS arguments properly', () => {
102+
const argv = {
103+
'chaincode-address': '0.0.0.0:9999',
104+
'chaincode-id': 'test_id:1',
105+
'module-path': '/tmp/example',
106+
'chaincode-tls-cert-path': certFileEncoded,
107+
'chaincode-tls-key-path': keyFileEncoded
108+
};
109+
110+
const ret = chaincodeServerCommand.getArgs({argv});
111+
112+
expect(ret.address).to.equal('0.0.0.0:9999');
113+
expect(ret.ccid).to.equal('test_id:1');
114+
115+
expect(ret.tlsProps).to.deep.equal({
116+
cert,
117+
key
118+
});
119+
});
120+
121+
it('should return the mutual TLS arguments properly', () => {
122+
const argv = {
123+
'chaincode-address': '0.0.0.0:9999',
124+
'chaincode-id': 'test_id:1',
125+
'module-path': '/tmp/example',
126+
'chaincode-tls-cert-path': certFileEncoded,
127+
'chaincode-tls-key-path': keyFileEncoded,
128+
'chaincode-tls-client-cacert-path': caFileEncoded
129+
};
130+
131+
const ret = chaincodeServerCommand.getArgs({argv});
132+
133+
expect(ret.address).to.equal('0.0.0.0:9999');
134+
expect(ret.ccid).to.equal('test_id:1');
135+
136+
expect(ret.tlsProps).to.deep.equal({
137+
cert,
138+
key,
139+
clientCACerts: ca
140+
});
141+
});
142+
143+
it('should return the TLS arguments with PEM files properly', () => {
144+
const argv = {
145+
'chaincode-address': '0.0.0.0:9999',
146+
'chaincode-id': 'test_id:1',
147+
'module-path': '/tmp/example',
148+
'chaincode-tls-cert-file': certFile,
149+
'chaincode-tls-key-file': keyFile,
150+
'chaincode-tls-client-cacert-file': caFile
151+
};
152+
153+
const ret = chaincodeServerCommand.getArgs({argv});
154+
155+
expect(ret.address).to.equal('0.0.0.0:9999');
156+
expect(ret.ccid).to.equal('test_id:1');
157+
158+
expect(ret.tlsProps).to.deep.equal({
159+
cert,
160+
key,
161+
clientCACerts: ca
162+
});
163+
});
164+
});
165+
166+
describe('parse arguments', () => {
167+
it('should parse the arguments successfully', () => {
168+
expect(() => {
169+
chaincodeServerCommand.builder(yargs)
170+
.exitProcess(false)
171+
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999');
172+
}).not.to.throw();
173+
});
174+
175+
it('should parse the arguments successfully with TLS options', () => {
176+
expect(() => {
177+
chaincodeServerCommand.builder(yargs)
178+
.exitProcess(false)
179+
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' +
180+
'--chaincode-tls-key-file tls.key --chaincode-tls-cert-file tls.pem');
181+
}).not.to.throw();
182+
});
183+
184+
it('should throw when conflicting arguments are passed', () => {
185+
expect(() => {
186+
chaincodeServerCommand.builder(yargs)
187+
.exitProcess(false)
188+
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' +
189+
'--chaincode-tls-key-file tls.key --chaincode-tls-key-path tls.pem');
190+
}).to.throw();
191+
});
192+
193+
it('should throw when only TLS key is passed', () => {
194+
expect(() => {
195+
chaincodeServerCommand.builder(yargs)
196+
.exitProcess(false)
197+
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' +
198+
'--chaincode-tls-key-file tls.key');
199+
}).to.throw();
200+
});
201+
202+
it('should throw when only TLS cert is passed', () => {
203+
expect(() => {
204+
chaincodeServerCommand.builder(yargs)
205+
.exitProcess(false)
206+
.parse('--chaincode-id test_id:1 --chaincode-address 0.0.0.0:9999 ' +
207+
'--chaincode-tls-cert-file tls.pem');
208+
}).to.throw();
209+
});
86210
});
87211
});

libraries/fabric-shim/test/unit/handler.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ const mockChaincodeImpl = {
3333
};
3434

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

3939
const mockOpts = {
4040
pem: ca,

0 commit comments

Comments
 (0)