From 2579307577abfdef656084f0be45010d1af1dd67 Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Mon, 27 Feb 2017 10:13:11 -0500 Subject: [PATCH] Allow fabric-ca-client to use per-instance crypto 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 --- fabric-ca-client/lib/FabricCAClientImpl.js | 29 +++++++------ .../lib/impl/CryptoSuite_ECDSA_AES.js | 18 +++++--- fabric-client/lib/msp/identity.js | 18 +++++++- fabric-client/lib/utils.js | 20 +++++++-- test/fixtures/docker-compose.yml | 2 +- test/integration/fabric-ca-services-tests.js | 41 ++++++++++++++----- test/unit/cryptosuite-ecdsa-aes.js | 4 ++ test/unit/identity.js | 8 ++++ 8 files changed, 102 insertions(+), 38 deletions(-) diff --git a/fabric-ca-client/lib/FabricCAClientImpl.js b/fabric-ca-client/lib/FabricCAClientImpl.js index be74914814..cdabb8269e 100644 --- a/fabric-ca-client/lib/FabricCAClientImpl.js +++ b/fabric-ca-client/lib/FabricCAClientImpl.js @@ -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); @@ -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 { @@ -285,7 +285,7 @@ var FabricCAClient = class { } this._baseAPI = '/api/v1/cfssl/'; - + this._cryptoPrimitives = cryptoPrimitives; } /** @@ -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); }); @@ -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) } }; @@ -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 { @@ -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)); @@ -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(/=*$/, ''); @@ -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; diff --git a/fabric-client/lib/impl/CryptoSuite_ECDSA_AES.js b/fabric-client/lib/impl/CryptoSuite_ECDSA_AES.js index 89481f3799..7861c552bb 100644 --- a/fabric-client/lib/impl/CryptoSuite_ECDSA_AES.js +++ b/fabric-client/lib/impl/CryptoSuite_ECDSA_AES.js @@ -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 = { @@ -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; @@ -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; diff --git a/fabric-client/lib/msp/identity.js b/fabric-client/lib/msp/identity.js index 1a318e7989..cd6fbec915 100644 --- a/fabric-client/lib/msp/identity.js +++ b/fabric-client/lib/msp/identity.js @@ -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); } }; diff --git a/fabric-client/lib/utils.js b/fabric-client/lib/utils.js index 008ed6ed99..04ff468852 100644 --- a/fabric-client/lib/utils.js +++ b/fabric-client/lib/utils.js @@ -42,6 +42,7 @@ 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 @@ -49,9 +50,16 @@ var sha3_256 = require('js-sha3').sha3_256; // @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'}) @@ -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]; @@ -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. diff --git a/test/fixtures/docker-compose.yml b/test/fixtures/docker-compose.yml index 482e0ad8b9..104a8fc855 100644 --- a/test/fixtures/docker-compose.yml +++ b/test/fixtures/docker-compose.yml @@ -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: diff --git a/test/integration/fabric-ca-services-tests.js b/test/integration/fabric-ca-services-tests.js index e9ed9b33de..8f5bc59897 100644 --- a/test/integration/fabric-ca-services-tests.js +++ b/test/integration/fabric-ca-services-tests.js @@ -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', @@ -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'); @@ -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 @@ -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'); @@ -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"'); @@ -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'); @@ -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(); diff --git a/test/unit/cryptosuite-ecdsa-aes.js b/test/unit/cryptosuite-ecdsa-aes.js index 5a83a63d80..a1f2649852 100644 --- a/test/unit/cryptosuite-ecdsa-aes.js +++ b/test/unit/cryptosuite-ecdsa-aes.js @@ -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(() => { @@ -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(() => { diff --git a/test/unit/identity.js b/test/unit/identity.js index 9bb748ee8f..203e31dd00 100644 --- a/test/unit/identity.js +++ b/test/unit/identity.js @@ -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');