Skip to content

Commit

Permalink
Implement fabric-ca revoke() client
Browse files Browse the repository at this point in the history
FAB-2475
Adding implementation of the revoke() call to fabric-ca

Change-Id: Icd2d00e09ee7bd21d13d35ed4ef6e53697aec7e1
Signed-off-by: Jim Zhang <[email protected]>
  • Loading branch information
jimthematrix committed Feb 24, 2017
1 parent 41fc4ea commit 651aac8
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 54 deletions.
153 changes: 119 additions & 34 deletions fabric-ca-client/lib/FabricCAClientImpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,9 @@ var FabricCAServices = class {
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()"');
}
checkRegistrar(registrar);

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());
return this._fabricCAClient.register(req.enrollmentID, 'client', req.group, req.attrs, registrar.getSigningIdentity());
}

/**
Expand Down Expand Up @@ -160,6 +150,41 @@ var FabricCAServices = class {
});
}

/**
* Revoke an existing certificate (enrollment certificate or transaction certificate), or revoke
* all certificates issued to an enrollment id. If revoking a particular certificate, then both
* the Authority Key Identifier and serial number are required. If revoking by enrollment id,
* then all future requests to enroll this id will be rejected.
* @param {Object} request Request object with the following fields:
* <br> - enrollmentID {string}. ID to revoke
* <br> - aki {string}. Authority Key Identifier string, hex encoded, for the specific certificate to revoke
* <br> - serial {string}. Serial number string, hex encoded, for the specific certificate to revoke
* <br> - reason {string}. The reason for revocation. See https://godoc.org/golang.org/x/crypto/ocsp
* for valid values. The default value is 0 (ocsp.Unspecified).
* @param {User} registrar The identity of the registrar (i.e. who is performing the revocation)
* @returns {Promise} The revocation results
*/
revoke(request, registrar) {
if (typeof request === 'undefined' || request === null) {
throw new Error('Missing required argument "request"');
}

if (request.enrollmentID === null || request.enrollmentID === '') {
if (request.aki === null || request.aki === '' || request.serial === null || request.serial === '') {
throw new Error('Enrollment ID is empty, thus both "aki" and "serial" must have non-empty values');
}
}

checkRegistrar(registrar);

return this._fabricCAClient.revoke(
request.enrollmentID,
request.aki,
request.serial,
(request.reason) ? request.reason : 0,
registrar.getSigningIdentity());
}

/**
* @typedef {Object} FabricCAServices-HTTPEndpoint
* @property {string} hostname
Expand Down Expand Up @@ -274,39 +299,92 @@ var FabricCAClient = class {
* @param {string} role Type of role for this user
* @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, signingIdentity) {
register(enrollmentID, role, group, attrs, signingIdentity) {

var self = this;
var numArgs = arguments.length;
//all arguments are required
if (numArgs < 5) {
throw new Error('Missing required parameters. \'enrollmentID\', \'role\', \'group\', \'attrs\', \
and \'signingIdentity\' are all required.');
}

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


var regRequest = {
'id': enrollmentID,
'type': role,
'group': group,
'attrs': attrs,
'callerID': callerID
'attrs': attrs
};

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());
}).catch(function (err) {
return reject(err);
});
});
}

/**
* Revoke an existing certificate (enrollment certificate or transaction certificate), or revoke
* all certificates issued to an enrollment id. If revoking a particular certificate, then both
* the Authority Key Identifier and serial number are required. If revoking by enrollment id,
* then all future requests to enroll this id will be rejected.
* @param {string} enrollmentID ID to revoke
* @param {string} aki Authority Key Identifier string, hex encoded, for the specific certificate to revoke
* @param {string} serial Serial number string, hex encoded, for the specific certificate to revoke
* @param {string} reason The reason for revocation. See https://godoc.org/golang.org/x/crypto/ocsp
* for valid values
* @param {SigningIdentity} signingIdentity The instance of a SigningIdentity encapsulating the
* signing certificate, hash algorithm and signature algorithm
* @returns {Promise} The revocation results
*/
revoke(enrollmentID, aki, serial, reason, signingIdentity) {

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

//all arguments are required
if (numArgs < 5) {
throw new Error('Missing required parameters. \'enrollmentID\', \'aki\', \'serial\', \'reason\', \
\'callerID\' and \'signingIdentity\' are all required.');
}

return new Promise(function (resolve, reject) {

var regRequest = {
'id': enrollmentID,
'aki': aki,
'serial': parseInt(serial, 16) + '', // per CFSSL, serial numbers are saved as decimals instead of hex strings in cert database
'reason': reason
};

return self.post('revoke', regRequest, signingIdentity)
.then(function (response) {
return resolve(response);
}).catch(function (err) {
return reject(err);
});
});
}

post(api_method, requestObj, signingIdentity) {
var self = this;
return new Promise(function (resolve, reject) {
var requestOptions = {
hostname: self._hostname,
port: self._port,
path: self._baseAPI + 'register',
path: self._baseAPI + api_method,
method: 'POST',
headers: {
Authorization: FabricCAClient.generateAuthToken(regRequest, signingIdentity)
Authorization: FabricCAClient.generateAuthToken(requestObj, signingIdentity)
}
};

Expand All @@ -327,15 +405,12 @@ var FabricCAClient = class {
}
//response should be JSON
try {
var regResponse = JSON.parse(payload);
if (regResponse.success) {
// 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());
var responseObj = JSON.parse(payload);
if (responseObj.success) {
return resolve(responseObj);
} else {
return reject(new Error(
util.format('Register failed with errors [%s]', JSON.stringify(regResponse.errors))));
util.format('Register failed with errors [%s]', JSON.stringify(responseObj.errors))));
}

} catch (err) {
Expand All @@ -350,12 +425,12 @@ var FabricCAClient = class {
reject(new Error(util.format('Calling register endpoint failed with error [%s]', err)));
});

request.write(JSON.stringify(regRequest));
request.write(JSON.stringify(requestObj));
request.end();
});
}

/**
/*
* Generate authorization token required for accessing fabric-ca APIs
*/
static generateAuthToken(reqBody, signingIdentity) {
Expand Down Expand Up @@ -507,5 +582,15 @@ var FabricCAClient = class {
}
};

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

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()"');
}
}

module.exports = FabricCAServices;
module.exports.FabricCAClient = FabricCAClient;
26 changes: 25 additions & 1 deletion test/integration/fabriccopservices-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ 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', [], 'admin', signingIdentity);
return cop._fabricCAClient.register(enrollmentID, 'client', 'bank_a', [], signingIdentity);
}).then((secret) => {
t.comment(secret);
enrollmentSecret = secret; // to be used in the next test case
Expand All @@ -108,6 +108,30 @@ test('FabricCAServices: Test enroll() With Dynamic CSR', function (t) {
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);

return cop.revoke({enrollmentID: 'testUserX'}, member);
}).then((response) => {
t.equal(response.success, true, 'Successfully revoked "testUserX"');

return cop.register({enrollmentID: 'testUserY', group: 'bank_a'}, member);
}).then((secret) => {
t.comment('Successfully registered another user "testUserY"');

return cop.enroll({enrollmentID: 'testUserY', enrollmentSecret: secret}, member);
}).then((enrollment) => {
t.comment('Successfully enrolled "testUserY"');

var cert = new X509();
cert.readCertPEM(enrollment.certificate);
var aki = X509.getExtAuthorityKeyIdentifier(cert.hex).kid;
var serial = cert.getSerialNumberHex();

t.comment(util.format('Ready to revoke certificate serial # "%s" with aki "%s"', serial, aki));

return cop.revoke({serial: serial, aki: aki}, member);
}).then((response) => {
t.equal(response.success, true, 'Successfully revoked "testUserY" using serial number and AKI');

t.end();
}).catch((err) => {
t.fail('Failed at ' + err.stack ? err.stack : err);
Expand Down
96 changes: 77 additions & 19 deletions test/unit/fabric-ca-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,25 +182,66 @@ test('FabricCAServices: Test register() function', function(t) {
() => {
cop.register({enrollmentID: 'testUser'}, {});
},
/Argument "registrar" must be an instance of the class "User", but is found to be missing a method "getName/,
/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'
);

cop.register({enrollmentID: 'testUser'}, { getSigningIdentity: function() { return 'dummy'; } })
.then(() => {
t.fail('Should not have been able to resolve this request');
t.end();
}).catch((err) => {
t.pass('Successfully rejected register call due to invalid parameters');

t.end();
});
});

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

t.throws(
() => {
cop.revoke({enrollmentID: null, aki: null, serial: '', reason: 0});
},
/Enrollment ID is empty, thus both "aki" and "serial" must have non-empty values/,
'Test missing both enrollmentID and aki/serial'
);
t.throws(
() => {
cop.register({enrollmentID: 'testUser'}, { getName: function() { return 'dummy';} });
cop.revoke({enrollmentID: null, aki: 'someId', serial: '', reason: 0});
},
/Enrollment ID is empty, thus both "aki" and "serial" must have non-empty values/,
'Test having valid "aki" but missing "serial"'
);
t.throws(
() => {
cop.revoke({enrollmentID: null, aki: '', serial: 'someId', reason: 0});
},
/Enrollment ID is empty, thus both "aki" and "serial" must have non-empty values/,
'Test having valid "serial" but missing "aki"'
);
t.throws(
() => {
cop.revoke({enrollmentID: null, aki: 'someId', serial: 'someId', reason: 0}, {});
},
/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'
'Test invalid "signingIdentity"'
);
t.doesNotThrow(
() => {
cop.register({enrollmentID: 'testUser'}, { getName: function() { return 'dummy'; }, getSigningIdentity: function() { return 'dummy'; } });
cop.revoke({enrollmentID: null, aki: 'someId', serial: 'someId', reason: 0}, {getSigningIdentity: function() { return 'dummy'; }})
.then(() => {
t.fail('Should not have been able to successfully resolved the revoke call');
t.end();
}).catch(() => {
t.pass('Successfully rejected the request to revoke due to invalid parameters');
t.end();
});
},
null,
'Should pass the argument checking but would fail when the register call tries to assemble the auth token'
'Test invalid "signingIdentity"'
);

t.end();
});

test('FabricCAServices: Test _parseURL() function', function (t) {
Expand Down Expand Up @@ -305,18 +346,35 @@ test('FabricCAClient: Test register with missing parameters', function (t) {
port: 7054
});

client.register()
.then(function (token) {
t.fail('Register must fail when missing required parameters');
t.end();
})
.catch(function (err) {
if (err.message.startsWith('Missing required parameters')) {
t.pass('Register should fail when missing required parameters');
} else {
t.fail('Register should have failed with \'Missing required parameters\'');
}
t.throws(
() => {
client.register();
},
/Missing required parameters/,
'Test missing all parameters'
);

t.end();
t.end();
});

/**
* FabricCAClient revoke tests
*/
test('FabricCAClient: Test revoke with missing parameters', function (t) {

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

t.throws(
() => {
client.revoke();
},
/Missing required parameters/,
'Test missing all parameters'
);

t.end();
});

0 comments on commit 651aac8

Please sign in to comment.