Skip to content

Commit

Permalink
Add authentication to register()
Browse files Browse the repository at this point in the history
FAB-1239
Add authentication header (cert + signature on (cert + body))
to the register() call in order to work with fabric-ca server
with authentication enabled.

Change-Id: I50e74f207b606b0ae199f318afb77bde540d9cd0
Signed-off-by: Jim Zhang <jzhang@us.ibm.com>
  • Loading branch information
jimthematrix committed Feb 24, 2017
1 parent 425028f commit 3473608
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 66 deletions.
76 changes: 55 additions & 21 deletions fabric-ca-client/lib/FabricCAClientImpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,35 @@ var FabricCAServices = class {

/**
* Register the member and return an enrollment secret.
* @param {Object} req Registration request with the following fields: enrollmentID, roles, registrar
* @param {Member} registrar The identity of the registrar (i.e. who is performing the registration)
* @returns Promise for the enrollmentSecret
* @ignore
* @param {Object} req Registration request with the following fields:
* <br> - enrollmentID {string}. ID which will be used for enrollment
* <br> - group {string}. Group to which this user will be assigned, like a company or an organization
* <br> - attrs {{@link KeyValueAttribute}[]}. Array of key/value attributes to assign to the user.
* @param registrar {User}. The identity of the registrar (i.e. who is performing the registration)
* @returns {Promise} The enrollment secret to use when this user enrolls
*/
register(req, registrar) {
var self = this;
if (typeof req === 'undefined' || req === null) {
throw new Error('Missing required argument "request"');
}

if (typeof req.enrollmentID === 'undefined' || req.enrollmentID === null) {
throw new Error('Missing required argument "request.enrollmentID"');
}

if (typeof registrar === 'undefined' || registrar === null) {
throw new Error('Missing required argument "registrar"');
}

if (typeof registrar.getName !== 'function') {
throw new Error('Argument "registrar" must be an instance of the class "User", but is found to be missing a method "getName()"');
}

if (typeof registrar.getSigningIdentity !== 'function') {
throw new Error('Argument "registrar" must be an instance of the class "User", but is found to be missing a method "getSigningIdentity()"');
}

return this._fabricCAClient.register(req.enrollmentID, 'client', req.group, req.attrs, registrar.getName(), registrar.getSigningIdentity());
}

/**
Expand Down Expand Up @@ -216,7 +237,6 @@ var FabricCAClient = class {
* @param {string} connect_opts.protocol The protocol to use (either HTTP or HTTPS)
* @param {string} connect_opts.hostname The hostname of the Fabric CA server endpoint
* @param {number} connect_opts.port The port of the Fabric CA server endpoint
* @param {Buffer[]} connect_opts.ca An array of trusted certificates in PEM format
* @throws Will throw an error if connection options are missing or invalid
*
*/
Expand All @@ -237,7 +257,6 @@ var FabricCAClient = class {
} else {
this._port = (connect_opts.protocol === 'http' ? 80 : 443);
}
this._ca = (connect_opts.ca) ? connect_opts.ca : null;
this._baseAPI = '/api/v1/cfssl/';


Expand All @@ -256,18 +275,20 @@ var FabricCAClient = class {
* @param {string} group Group to which this user will be assigned
* @param {KeyValueAttribute[]} attrs Array of key/value attributes to assign to the user
* @param {string} callerID The ID of the user who is registering this user
* @param {SigningIdentity} signingIdentity The instance of a SigningIdentity encapsulating the
* signing certificate, hash algorithm and signature algorithm
* @returns {Promise} The enrollment secret to use when this user enrolls
*/
register(enrollmentID, role, group, attrs, callerID) {
register(enrollmentID, role, group, attrs, callerID, signingIdentity) {

var self = this;
var numArgs = arguments.length;

return new Promise(function (resolve, reject) {
//all arguments are required
if (numArgs < 5) {
if (numArgs < 6) {
reject(new Error('Missing required parameters. \'enrollmentID\', \'role\', \'group\', \'attrs\', \
and \'callerID\' are all required.'));
\'callerID\' and \'signingIdentity\' are all required.'));
}


Expand All @@ -284,8 +305,9 @@ var FabricCAClient = class {
port: self._port,
path: self._baseAPI + 'register',
method: 'POST',
//auth: enrollmentID + ':' + enrollmentSecret,
ca: self._ca
headers: {
Authorization: FabricCAClient.generateAuthToken(regRequest, signingIdentity)
}
};

var request = self._httpClient.request(requestOptions, function (response) {
Expand All @@ -307,8 +329,10 @@ var FabricCAClient = class {
try {
var regResponse = JSON.parse(payload);
if (regResponse.success) {
//we want the result field which is Base64-encoded PEM
return resolve(regResponse.result);
// we want the result field which is Base64-encoded secret.
// TODO: Keith said this may be changed soon for 'result' to be the raw secret
// without Base64-encoding it
return resolve(Buffer.from(regResponse.result, 'base64').toString());
} else {
return reject(new Error(
util.format('Register failed with errors [%s]', JSON.stringify(regResponse.errors))));
Expand All @@ -332,13 +356,24 @@ var FabricCAClient = class {
}

/**
* Generate authorization token required for accessing fabric-cop APIs
* @param {string} privKey The pem-encoded private key used for signing
* @param {string} X509Key The pem-encoded X509 certificate associated with privKey
* @param {string} reqBody The body of the request to sign as part of the token
* Generate authorization token required for accessing fabric-ca APIs
*/
static generateAuthToken(privKey, X509Key, reqBody) {
static generateAuthToken(reqBody, signingIdentity) {
// sometimes base64 encoding results in trailing one or two "=" as padding
var trim = function(string) {
return string.replace(/=*$/, '');
};

// specific signing procedure is according to:
// https://github.com/hyperledger/fabric-ca/blob/master/util/util.go#L213
var cert = trim(Buffer.from(signingIdentity._certificate).toString('base64'));
var body = trim(Buffer.from(JSON.stringify(reqBody)).toString('base64'));

var bodyAndcert = body + '.' + cert;
var sig = signingIdentity.sign(bodyAndcert);

var b64Sign = trim(Buffer.from(sig, 'hex').toString('base64'));
return cert + '.' + b64Sign;
}

/**
Expand Down Expand Up @@ -366,8 +401,7 @@ var FabricCAClient = class {
port: self._port,
path: self._baseAPI + 'enroll',
method: 'POST',
auth: enrollmentID + ':' + enrollmentSecret,
ca: self._ca
auth: enrollmentID + ':' + enrollmentSecret
};

var enrollRequest = {
Expand Down
107 changes: 62 additions & 45 deletions test/integration/fabriccopservices-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,21 @@ var fs = require('fs');
var path = require('path');
var testUtil = require('../unit/util.js');
var utils = require('fabric-client/lib/utils.js');
var LocalMSP = require('fabric-client/lib/msp/msp.js');
var idModule = require('fabric-client/lib/msp/identity.js');
var SigningIdentity = idModule.SigningIdentity;
var Signer = idModule.Signer;
var User = require('fabric-client/lib/User.js');

var keyValStorePath = testUtil.KVS;


var FabricCAServices = require('fabric-ca-client/lib/FabricCAClientImpl');
var FabricCAClient = FabricCAServices.FabricCAClient;

var enrollmentID = 'testUser';
var enrollmentSecret = 'user1';
var enrollmentSecret;
var csr = fs.readFileSync(path.resolve(__dirname, '../fixtures/fabriccop/enroll-csr.pem'));


test('FabricCAClient: Test enroll With Static CSR', function (t) {

var client = new FabricCAClient({
protocol: 'http',
hostname: '127.0.0.1',
port: 7054
});

//
return client.enroll(enrollmentID, enrollmentSecret, csr.toString())
.then(function (pem) {
t.comment(pem);
t.pass('Successfully invoked enroll API with enrollmentID \'' + enrollmentID + '\'');
//check that we got back the expected certificate
var cert = new X509();
cert.readCertPEM(pem);
t.comment(cert.getSubjectString());
t.equal(cert.getSubjectString(), '/CN=' + enrollmentID, 'Subject should be /CN=' + enrollmentID);
})
.catch(function (err) {
t.fail('Failed to enroll \'' + enrollmentID + '\'. ' + err);
});
});

/**
* FabricCAServices class tests
*/
Expand All @@ -83,43 +62,81 @@ test('FabricCAServices: Test enroll() With Dynamic CSR', function (t) {
enrollmentSecret: 'adminpw'
};

var eResult, client, member;
return cop.enroll(req)
.then(
function (enrollment) {

.then((enrollment) => {
t.pass('Successfully enrolled \'' + req.enrollmentID + '\'.');
eResult = enrollment;

//check that we got back the expected certificate
var cert = new X509();
cert.readCertPEM(enrollment.certificate);
t.comment(cert.getSubjectString());
t.equal(cert.getSubjectString(), '/CN=' + req.enrollmentID, 'Subject should be /CN=' + req.enrollmentID);
},
function (err) {
t.fail('Failed to enroll \'' + req.enrollmentID + '\'. ' + err);
}
);

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

var msp = new LocalMSP({
id: 'DEFAULT',
cryptoSuite: cop.cryptoPrimitives
});

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

return cop._fabricCAClient.register(enrollmentID, 'client', 'bank_a', [], 'admin', signingIdentity);
}).then((secret) => {
t.comment(secret);
enrollmentSecret = secret; // to be used in the next test case

t.pass('Successfully invoked register API with enrollmentID \'' + enrollmentID + '\'');

return hfc.newDefaultKeyValueStore({
path: testUtil.KVS
});
}).then((store) => {
t.comment('Successfully constructed a state store');

client = new hfc();
client.setStateStore(store);
member = new User('adminX', client);
return member.setEnrollment(eResult.key, eResult.certificate);
}).then(() => {
t.comment('Successfully constructed a user object based on the enrollment');

return cop.register({enrollmentID: 'testUserX', group: 'bank_a'}, member);
}).then((secret) => {
t.pass('Successfully enrolled "testUserX" in group "bank_a" with enrollment secret returned: ' + secret);
t.end();
}).catch((err) => {
t.fail('Failed at ' + err.stack ? err.stack : err);
t.end();
});
});

test('FabricCAClient: Test register', function (t) {
test('FabricCAClient: Test enroll With Static CSR', function (t) {

var client = new FabricCAClient({
protocol: 'http',
hostname: '127.0.0.1',
port: 7054
});

var enrollmentID = 'testRegisterUser';


return client.register(enrollmentID, 'client', 'bank_a', [], 'admin')
.then(function (secret) {
t.comment(secret);
t.pass('Successfully invoked register API with enrollmentID \'' + enrollmentID + '\'');

t.comment(util.format('Sending enroll request for user %s with enrollment secret %s', enrollmentID, enrollmentSecret));
return client.enroll(enrollmentID, enrollmentSecret, csr.toString())
.then(function (pem) {
t.comment(pem);
t.pass('Successfully invoked enroll API with enrollmentID \'' + enrollmentID + '\'');
//check that we got back the expected certificate
var cert = new X509();
cert.readCertPEM(pem);
t.comment(cert.getSubjectString());
t.equal(cert.getSubjectString(), '/CN=' + enrollmentID, 'Subject should be /CN=' + enrollmentID);
t.end();
})
.catch(function (err) {
t.fail('Failed to register \'' + enrollmentID + '\'. ' + err);
t.fail('Failed to enroll \'' + enrollmentID + '\'. ' + err);
t.end();
});
});
58 changes: 58 additions & 0 deletions test/unit/fabric-ca-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,62 @@ test('FabricCAClient: Test _pemToDer static method',function(t){
t.end();
});

test('FabricCAServices: Test register() function', function(t) {
var cop = new FabricCAServices('http://localhost:7054');

t.throws(
() => {
cop.register();
},
/Missing required argument "request"/,
'Must fail if missing request argument'
);
t.throws(
() => {
cop.register({});
},
/Missing required argument "request.enrollmentID"/,
'Must fail if missing request.enrollmentID argument'
);
t.throws(
() => {
cop.register({dummy: 'value'});
},
/Missing required argument "request.enrollmentID"/,
'Must fail if missing request argument'
);
t.throws(
() => {
cop.register({enrollmentID: 'testUser'});
},
/Missing required argument "registrar"/,
'Must fail if missing registrar argument'
);
t.throws(
() => {
cop.register({enrollmentID: 'testUser'}, {});
},
/Argument "registrar" must be an instance of the class "User", but is found to be missing a method "getName/,
'Must fail if registrar argument is not a User object'
);
t.throws(
() => {
cop.register({enrollmentID: 'testUser'}, { getName: function() { return 'dummy';} });
},
/Argument "registrar" must be an instance of the class "User", but is found to be missing a method "getSigningIdentity/,
'Must fail if registrar argument is not a User object'
);
t.doesNotThrow(
() => {
cop.register({enrollmentID: 'testUser'}, { getName: function() { return 'dummy'; }, getSigningIdentity: function() { return 'dummy'; } });
},
null,
'Should pass the argument checking but would fail when the register call tries to assemble the auth token'
);

t.end();
});

test('FabricCAServices: Test _parseURL() function', function (t) {

var goodHost = 'www.example.com';
Expand Down Expand Up @@ -207,6 +263,8 @@ test('FabricCAServices: Test _parseURL() function', function (t) {
/InvalidURL: url must start with http or https./,
'Throw error for missing protocol'
);

t.end();
});

/**
Expand Down

0 comments on commit 3473608

Please sign in to comment.