Skip to content

Commit

Permalink
Allow fabric-ca-client to use per-instance crypto
Browse files Browse the repository at this point in the history
FAB-2497
allow crypto parameters (keysize, hash algo, etc.) to be passed
into the constructor of the client instance, rather than requiring
the app to call setConfigSetting() which is clunky and would
impact global settings.

as part of this also need to allow a hash function to be passed
into the "sign()" method in the SigningIdentity class because
the same signing identity may need to use different hashing
algorithms (like when talking to fabric-ca vs. peer/orderers)

Change-Id: Id21fe7c2b5b120f11f04cdd31f624fce687e4361
Signed-off-by: Jim Zhang <jzhang@us.ibm.com>
  • Loading branch information
jimthematrix committed Feb 27, 2017
1 parent bc36ef5 commit 2579307
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 38 deletions.
29 changes: 14 additions & 15 deletions fabric-ca-client/lib/FabricCAClientImpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ var FabricCAServices = class {

var endpoint = FabricCAServices._parseURL(url);

this.cryptoPrimitives = utils.getCryptoSuite(cryptoSettings, KVSImplClass, opts);

this._fabricCAClient = new FabricCAClient({
protocol: endpoint.protocol,
hostname: endpoint.hostname,
port: endpoint.port
});

this.cryptoPrimitives = utils.getCryptoSuite(cryptoSettings, KVSImplClass, opts);
}, this.cryptoPrimitives);

logger.info('Successfully constructed Fabric CA service client: endpoint - %j', endpoint);

Expand Down Expand Up @@ -266,7 +266,7 @@ var FabricCAClient = class {
* @throws Will throw an error if connection options are missing or invalid
*
*/
constructor(connect_opts) {
constructor(connect_opts, cryptoPrimitives) {

//check connect_opts
try {
Expand All @@ -285,7 +285,7 @@ var FabricCAClient = class {
}
this._baseAPI = '/api/v1/cfssl/';


this._cryptoPrimitives = cryptoPrimitives;
}

/**
Expand Down Expand Up @@ -324,9 +324,7 @@ var FabricCAClient = class {

return self.post('register', regRequest, signingIdentity)
.then(function (response) {
// TODO: Keith said this may be changed soon for 'result' to be the raw secret
// without Base64-encoding it
return resolve(Buffer.from(response.result, 'base64').toString());
return resolve(response.result.credential);
}).catch(function (err) {
return reject(err);
});
Expand Down Expand Up @@ -385,7 +383,7 @@ var FabricCAClient = class {
path: self._baseAPI + api_method,
method: 'POST',
headers: {
Authorization: FabricCAClient.generateAuthToken(requestObj, signingIdentity)
Authorization: self.generateAuthToken(requestObj, signingIdentity)
}
};

Expand All @@ -402,7 +400,7 @@ var FabricCAClient = class {

if (!payload) {
reject(new Error(
util.format('Registerfailed with HTTP status code ', response.statusCode)));
util.format('fabric-ca request %s failed with HTTP status code %s', api_method, response.statusCode)));
}
//response should be JSON
try {
Expand All @@ -411,19 +409,19 @@ var FabricCAClient = class {
return resolve(responseObj);
} else {
return reject(new Error(
util.format('Register failed with errors [%s]', JSON.stringify(responseObj.errors))));
util.format('fabric-ca request %s failed with errors [%s]', api_method, JSON.stringify(responseObj.errors))));
}

} catch (err) {
reject(new Error(
util.format('Could not parse register response [%s] as JSON due to error [%s]', payload, err)));
util.format('Could not parse %s response [%s] as JSON due to error [%s]', api_method, payload, err)));
}
});

});

request.on('error', function (err) {
reject(new Error(util.format('Calling register endpoint failed with error [%s]', err)));
reject(new Error(util.format('Calling %s endpoint failed with error [%s]', api_method, err)));
});

request.write(JSON.stringify(requestObj));
Expand All @@ -434,7 +432,7 @@ var FabricCAClient = class {
/*
* Generate authorization token required for accessing fabric-ca APIs
*/
static generateAuthToken(reqBody, signingIdentity) {
generateAuthToken(reqBody, signingIdentity) {
// sometimes base64 encoding results in trailing one or two "=" as padding
var trim = function(string) {
return string.replace(/=*$/, '');
Expand All @@ -446,7 +444,8 @@ var FabricCAClient = class {
var body = trim(Buffer.from(JSON.stringify(reqBody)).toString('base64'));

var bodyAndcert = body + '.' + cert;
var sig = signingIdentity.sign(bodyAndcert);
var sig = signingIdentity.sign(bodyAndcert, { hashFunction: this._cryptoPrimitives.hash.bind(this._cryptoPrimitives) });
logger.debug(util.format('bodyAndcert: %s', bodyAndcert));

var b64Sign = trim(Buffer.from(sig, 'hex').toString('base64'));
return cert + '.' + b64Sign;
Expand Down
18 changes: 12 additions & 6 deletions fabric-client/lib/impl/CryptoSuite_ECDSA_AES.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,20 @@ var CryptoSuite_ECDSA_AES = class extends api.CryptoSuite {
* @param {string} KVSImplClass Optional. The built-in key store saves private keys. The key store may be backed by different
* {@link KeyValueStore} implementations. If specified, the value of the argument must point to a module implementing the
* KeyValueStore interface.
* @param {string} hash Optional. Hash algorithm, supported values are "SHA2" and "SHA3"
*/
constructor(keySize, opts, KVSImplClass) {
constructor(keySize, opts, KVSImplClass, hash) {
super();

if (keySize !== 256 && keySize !== 384) {
throw new Error('Illegal key size: ' + keySize + ' - this crypto suite only supports key sizes 256 or 384');
}

super();
if (typeof hash === 'string' && hash !== null && hash !== '') {
this._hashAlgo = hash;
} else {
this._hashAlgo = utils.getConfigSetting('crypto-hash-algo');
}

if (typeof opts === 'undefined' || opts === null) {
opts = {
Expand Down Expand Up @@ -100,11 +107,10 @@ var CryptoSuite_ECDSA_AES = class extends api.CryptoSuite {

// hash function must be set carefully to produce the hash size compatible with the key algorithm
// https://www.ietf.org/rfc/rfc5480.txt (see page 9 "Recommended key size, digest algorithm and curve")
var hashAlgo = utils.getConfigSetting('crypto-hash-algo');

logger.debug('Hash algorithm: %s, hash output size: %s', hashAlgo, this._keySize);
logger.debug('Hash algorithm: %s, hash output size: %s', this._hashAlgo, this._keySize);

switch (hashAlgo.toLowerCase() + '-' + this._keySize) {
switch (this._hashAlgo.toLowerCase() + '-' + this._keySize) {
case 'sha3-256':
this._hashFunction = hashPrimitives.sha3_256;
this._hashFunctionKeyDerivation = hashPrimitives.hash_sha3_256;
Expand All @@ -122,7 +128,7 @@ var CryptoSuite_ECDSA_AES = class extends api.CryptoSuite {
//TODO: this._hashFunctionKeyDerivation = xxxxxxx;
break;
default:
throw Error(util.format('Unsupported hash algorithm and key size pair: %s-%s', hashAlgo, this._keySize));
throw Error(util.format('Unsupported hash algorithm and key size pair: %s-%s', this._hashAlgo, this._keySize));
}

this._hashOutputSize = this._keySize / 8;
Expand Down
18 changes: 16 additions & 2 deletions fabric-client/lib/msp/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,24 @@ var SigningIdentity = class extends Identity {
* Signs digest with the private key contained inside the signer.
*
* @param {byte[]} msg The message to sign
* @param {object} opts Options object for the signing, contains one field 'hashFunction' that allows
* different hashing algorithms to be used. If not present, will default to the hash function
* configured for the identity's own crypto suite object
*/
sign(msg) {
sign(msg, opts) {
// calculate the hash for the message before signing
var digest = this._msp.cryptoSuite.hash(msg);
var hashFunction;
if (opts && opts.hashFunction) {
if (typeof opts.hashFunction !== 'function') {
throw new Error('The "hashFunction" field must be a function');
}

hashFunction = opts.hashFunction;
} else {
hashFunction = this._msp.cryptoSuite.hash.bind(this._msp.cryptoSuite);
}

var digest = hashFunction(msg);
return this._signer.sign(Buffer.from(digest, 'hex'), null);
}
};
Expand Down
20 changes: 17 additions & 3 deletions fabric-client/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,24 @@ var sha3_256 = require('js-sha3').sha3_256;
// in the setting 'crypto-suite-software'
// - keysize {number}: The key size to use for the crypto suite instance. default is value of the setting 'crypto-keysize'
// - algorithm {string}: Digital signature algorithm, currently supporting ECDSA only with value "EC"
// - hash {string}: 'SHA2' or 'SHA3'
//
// @param {function} KVSImplClass Optional. The built-in key store saves private keys. The key store may be backed by different
// {@link KeyValueStore} implementations. If specified, the value of the argument must point to a module implementing the
// KeyValueStore interface.
// @param {object} opts Implementation-specific option object used in the constructor
//
module.exports.getCryptoSuite = function(setting, KVSImplClass, opts) {
var csImpl, keysize, algorithm, haveSettings = false;
var csImpl, keysize, algorithm, hashAlgo, haveSettings = false;

csImpl = this.getConfigSetting('crypto-hsm') ? this.getConfigSetting('crypto-suite-hsm') : this.getConfigSetting('crypto-suite-software');
var useHSM = false;
if (setting && typeof setting.software === 'boolean') {
useHSM = !setting.software;
} else {
useHSM = this.getConfigSetting('crypto-hsm');
}

csImpl = useHSM ? this.getConfigSetting('crypto-suite-hsm') : this.getConfigSetting('crypto-suite-software');

// this function supports skipping any of the arguments such that it can be called in any of the following fashions:
// - getCryptoSuite({software: true, keysize: 256, algorithm: EC}, CouchDBKeyValueStore, {name: 'member_db', url: 'http://localhost:5984'})
Expand All @@ -74,6 +82,12 @@ module.exports.getCryptoSuite = function(setting, KVSImplClass, opts) {
} else
algorithm = 'EC';

if (typeof setting === 'object' && typeof setting.hash === 'string') {
hashAlgo = setting.hash.toUpperCase();
haveSettings = true;
} else
hashAlgo = null;

// csImpl at this point should be a map (see config/default.json) with keys being the algorithm
csImpl = csImpl[algorithm];

Expand Down Expand Up @@ -111,7 +125,7 @@ module.exports.getCryptoSuite = function(setting, KVSImplClass, opts) {
}
}

return new cryptoSuite(keysize, opts, keystoreSuperClass);
return new cryptoSuite(keysize, opts, keystoreSuperClass, hashAlgo);
};

// Provide a Promise-based keyValueStore for couchdb, etc.
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
- FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
ports:
- "7054:7054"
command: sh -c 'fabric-ca-server start -b admin:adminpw'
command: sh -c 'fabric-ca-server start -b admin:adminpw' -d
container_name: ca

orderer:
Expand Down
41 changes: 30 additions & 11 deletions test/integration/fabric-ca-services-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,7 @@ var csr = fs.readFileSync(path.resolve(__dirname, '../fixtures/fabriccop/enroll-

test('FabricCAServices: Test enroll() With Dynamic CSR', function (t) {

// need to override the default key size 384 to match the member service backend
// otherwise the client will not be able to decrypt the enrollment challenge
utils.setConfigSetting('crypto-keysize', 256);

var cop = new FabricCAServices('http://localhost:7054');
var cop = new FabricCAServices('http://localhost:7054', {keysize: 256, hash: 'SHA2'});

var req = {
enrollmentID: 'admin',
Expand All @@ -75,6 +71,10 @@ test('FabricCAServices: Test enroll() With Dynamic CSR', function (t) {
t.equal(cert.getSubjectString(), '/CN=' + req.enrollmentID, 'Subject should be /CN=' + req.enrollmentID);

return cop.cryptoPrimitives.importKey(enrollment.certificate);
},(err) => {
t.fail('Failed to enroll the admin. Can not progress any further. Exiting. ' + err.stack ? err.stack : err);

t.end();
}).then((pubKey) => {
t.pass('Successfully imported public key from the resulting enrollment certificate');

Expand All @@ -85,7 +85,10 @@ test('FabricCAServices: Test enroll() With Dynamic CSR', function (t) {

var signingIdentity = new SigningIdentity('testSigningIdentity', eResult.certificate, pubKey, msp, new Signer(msp.cryptoSuite, eResult.key));

return cop._fabricCAClient.register(enrollmentID, 'client', 'bank_a', [], signingIdentity);
return cop._fabricCAClient.register(enrollmentID, 'client', 'org1', [], signingIdentity);
},(err) => {
t.fail('Failed to import the public key from the enrollment certificate. ' + err.stack ? err.stack : err);
t.end();
}).then((secret) => {
t.comment(secret);
enrollmentSecret = secret; // to be used in the next test case
Expand All @@ -95,6 +98,9 @@ test('FabricCAServices: Test enroll() With Dynamic CSR', function (t) {
return hfc.newDefaultKeyValueStore({
path: testUtil.KVS
});
},(err) => {
t.fail(util.format('Failed to register "%s". %s', enrollmentID, err.stack ? err.stack : err));
t.end();
}).then((store) => {
t.comment('Successfully constructed a state store');

Expand All @@ -105,15 +111,28 @@ test('FabricCAServices: Test enroll() With Dynamic CSR', function (t) {
}).then(() => {
t.comment('Successfully constructed a user object based on the enrollment');

return cop.register({enrollmentID: 'testUserX', group: 'bank_a'}, member);
return cop.register({enrollmentID: 'testUserX', group: 'bank_X'}, member);
}).then((secret) => {
t.pass('Successfully enrolled "testUserX" in group "bank_a" with enrollment secret returned: ' + secret);
t.fail('Should not have been able to register user of a group "bank_X" because "admin" does not belong to that group');
t.end();
},(err) => {
t.pass('Successfully rejected registration request "testUserX" in group "bank_X"');

return cop.register({enrollmentID: 'testUserX', group: 'org1'}, member);
}).then((secret) => {
t.pass('Successfully registered "testUserX" in group "org1" with enrollment secret returned: ' + secret);

return cop.revoke({enrollmentID: 'testUserX'}, member);
},(err) => {
t.fail('Failed to register "testUserX". ' + err.stack ? err.stack : err);
t.end();
}).then((response) => {
t.equal(response.success, true, 'Successfully revoked "testUserX"');

return cop.register({enrollmentID: 'testUserY', group: 'bank_a'}, member);
return cop.register({enrollmentID: 'testUserY', group: 'org2.department1'}, member);
},(err) => {
t.fail('Failed to revoke "testUserX". ' + err.stack ? err.stack : err);
t.end();
}).then((secret) => {
t.comment('Successfully registered another user "testUserY"');

Expand All @@ -133,7 +152,7 @@ test('FabricCAServices: Test enroll() With Dynamic CSR', function (t) {
t.equal(response.success, true, 'Successfully revoked "testUserY" using serial number and AKI');

// register a new user 'webAdmin' that can register other users of the role 'client'
return cop.register({enrollmentID: 'webAdmin', group: 'bank_a', attrs: [{name: 'hf.Registrar.Roles', value: 'client'}]}, member);
return cop.register({enrollmentID: 'webAdmin', group: 'org1.department2', attrs: [{name: 'hf.Registrar.Roles', value: 'client'}]}, member);
}).then((secret) => {
t.pass('Successfully registered "webAdmin" who can register other users of the "client" role');

Expand All @@ -156,7 +175,7 @@ test('FabricCAServices: Test enroll() With Dynamic CSR', function (t) {
},(err) => {
t.pass('Successfully rejected attempt to register a user of invalid role. ' + err);

return cop.register({enrollmentID: 'auditor', role: 'client', group: 'bank_a'}, webAdmin);
return cop.register({enrollmentID: 'auditor', role: 'client', group: 'org2.department1'}, webAdmin);
}).then(() => {
t.pass('Successfully registered "auditor" of role "client" from "webAdmin"');
t.end();
Expand Down
4 changes: 4 additions & 0 deletions test/unit/cryptosuite-ecdsa-aes.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ test('\n\n** utils.getCryptoSuite tests **\n\n', (t) => {
'Load the CryptoSuite_ECDSA_AES module and pass in an invalid config object'
);

// make sure the "software: true" setting overrides the config setting
utils.setConfigSetting('crypto-hsm', true);
cs = utils.getCryptoSuite({software: true, algorithm: 'EC', keysize: 256}, CouchDBKeyValueStore, { name: 'test_db', url: 'http://dummyUrl' });
cs._getKeyStore()
.then(() => {
Expand All @@ -207,6 +209,8 @@ test('\n\n ** CryptoSuite_ECDSA_AES - constructor tests **\n\n', function (t) {
var keyValueStore = null;
let cs;

utils.setConfigSetting('crypto-hsm', false);

cs = new CryptoSuite_ECDSA_AES(256, { name: 'test_db', url: 'http://dummyUrl'}, CouchDBKeyValueStore);
cs._getKeyStore()
.then(() => {
Expand Down
8 changes: 8 additions & 0 deletions test/unit/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,14 @@ test('\n\n ** Identity class tests **\n\n', function (t) {

var signingID = new SigningIdentity('testSigningIdentity', TEST_KEY_PRIVATE_CERT_PEM, pubKey, mspImpl, signer);

t.throws(
() => {
signingID.sign(TEST_MSG, {hashFunction: 'not_a_function'});
},
/The "hashFunction" field must be a function/,
'Test invalid hashFunction parameter for the sign() method'
);

var sig = signingID.sign(TEST_MSG);
t.equal(cryptoUtils.verify(pubKey, sig, TEST_MSG), true, 'Test SigningIdentity sign() method');
t.equal(signingID.verify(TEST_MSG, sig), true, 'Test Identity verify() method');
Expand Down

0 comments on commit 2579307

Please sign in to comment.