From 270a75e97042826ba3dc6f120a2506fec8187659 Mon Sep 17 00:00:00 2001 From: Saad Karim Date: Thu, 7 Dec 2017 11:16:12 -0500 Subject: [PATCH] [FAB-7127] Revoke missing gencrl Fabric CA supports generating a CRL on the revoke, this is now also supported rom the JSDK api. Also fixes the NPE if reason is null when making the revoke request. Change-Id: I1fb39089db9e3dd828a7f6a8da1d65eea6cf2f6d Signed-off-by: Saad Karim --- .../hyperledger/fabric_ca/sdk/HFCAClient.java | 71 +++++++- .../fabric_ca/sdk/RevocationRequest.java | 25 ++- .../sdk/exception/RevocationException.java | 4 + .../fabric_ca/sdk/RevocationRequestTest.java | 4 +- .../sdkintegration/HFCAClientIT.java | 158 +++++++++++++++--- 5 files changed, 232 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/hyperledger/fabric_ca/sdk/HFCAClient.java b/src/main/java/org/hyperledger/fabric_ca/sdk/HFCAClient.java index 5159517a..6d2b1a9d 100644 --- a/src/main/java/org/hyperledger/fabric_ca/sdk/HFCAClient.java +++ b/src/main/java/org/hyperledger/fabric_ca/sdk/HFCAClient.java @@ -516,7 +516,26 @@ public Enrollment reenroll(User user, EnrollmentRequest req) throws EnrollmentEx * @throws InvalidArgumentException */ - public void revoke(User revoker, Enrollment enrollment, String reason) throws RevocationException, InvalidArgumentException { + public void revoke(User revoker, Enrollment enrollment, String reason) throws RevocationException, InvalidArgumentException { + revokeInternal(revoker, enrollment, reason, false); + } + + /** + * revoke one enrollment of user + * + * @param revoker admin user who has revoker attribute configured in CA-server + * @param enrollment the user enrollment to be revoked + * @param reason revoke reason, see RFC 5280 + * @param genCRL generate CRL list + * @throws RevocationException + * @throws InvalidArgumentException + */ + + public String revoke(User revoker, Enrollment enrollment, String reason, boolean genCRL) throws RevocationException, InvalidArgumentException { + return revokeInternal(revoker, enrollment, reason, genCRL); + } + + private String revokeInternal(User revoker, Enrollment enrollment, String reason, boolean genCRL) throws RevocationException, InvalidArgumentException { if (cryptoSuite == null) { throw new InvalidArgumentException("Crypto primitives not set."); @@ -549,14 +568,25 @@ public void revoke(User revoker, Enrollment enrollment, String reason) throws Re String aki = DatatypeConverter.printHexBinary(AuthorityKeyIdentifier.getInstance(akiOc.getOctets()).getKeyIdentifier()); // build request body - RevocationRequest req = new RevocationRequest(caName, null, serial, aki, reason); + RevocationRequest req = new RevocationRequest(caName, null, serial, aki, reason, genCRL); String body = req.toJson(); String authHdr = getHTTPAuthCertificate(revoker.getEnrollment(), body); // send revoke request - httpPost(url + HFCA_REVOKE, body, authHdr); + JsonObject resp = httpPost(url + HFCA_REVOKE, body, authHdr); logger.debug("revoke done"); + + if (genCRL) { + if (resp.isEmpty()) { + throw new RevocationException("Failed to return CRL, revoke response is empty"); + } + if (resp.isNull("CRL")) { + throw new RevocationException("Failed to return CRL"); + } + return resp.getString("CRL"); + } + return null; } catch (CertificateException e) { logger.error("Cannot validate certificate. Error is: " + e.getMessage()); throw new RevocationException("Error while revoking cert. " + e.getMessage(), e); @@ -578,6 +608,25 @@ public void revoke(User revoker, Enrollment enrollment, String reason) throws Re */ public void revoke(User revoker, String revokee, String reason) throws RevocationException, InvalidArgumentException { + revokeInternal(revoker, revokee, reason, false); + } + + /** + * revoke one user (including his all enrollments) + * + * @param revoker admin user who has revoker attribute configured in CA-server + * @param revokee user who is to be revoked + * @param reason revoke reason, see RFC 5280 + * @param genCRL generate CRL + * @throws RevocationException + * @throws InvalidArgumentException + */ + + public String revoke(User revoker, String revokee, String reason, boolean genCRL) throws RevocationException, InvalidArgumentException { + return revokeInternal(revoker, revokee, reason, genCRL); + } + + private String revokeInternal(User revoker, String revokee, String reason, boolean genCRL) throws RevocationException, InvalidArgumentException { if (cryptoSuite == null) { throw new InvalidArgumentException("Crypto primitives not set."); @@ -596,15 +645,27 @@ public void revoke(User revoker, String revokee, String reason) throws Revocatio setUpSSL(); // build request body - RevocationRequest req = new RevocationRequest(caName, revokee, null, null, reason); + RevocationRequest req = new RevocationRequest(caName, revokee, null, null, reason, genCRL); String body = req.toJson(); // build auth header String authHdr = getHTTPAuthCertificate(revoker.getEnrollment(), body); // send revoke request - httpPost(url + HFCA_REVOKE, body, authHdr); + JsonObject resp = httpPost(url + HFCA_REVOKE, body, authHdr); + logger.debug(format("revoke revokee: %s done.", revokee)); + + if (genCRL) { + if (resp.isEmpty()) { + throw new RevocationException("Failed to return CRL, revoke response is empty"); + } + if (resp.isNull("CRL")) { + throw new RevocationException("Failed to return CRL"); + } + return resp.getString("CRL"); + } + return null; } catch (Exception e) { logger.error(e.getMessage(), e); throw new RevocationException("Error while revoking the user. " + e.getMessage(), e); diff --git a/src/main/java/org/hyperledger/fabric_ca/sdk/RevocationRequest.java b/src/main/java/org/hyperledger/fabric_ca/sdk/RevocationRequest.java index 96071b75..9e0cbc6c 100644 --- a/src/main/java/org/hyperledger/fabric_ca/sdk/RevocationRequest.java +++ b/src/main/java/org/hyperledger/fabric_ca/sdk/RevocationRequest.java @@ -38,9 +38,11 @@ class RevocationRequest { private String aki; // Reason for revocation private String reason; + // Get CRL list back as part of revoke request + private Boolean genCRL; // Constructor - RevocationRequest(String caNmae, String id, String serial, String aki, String reason) throws Exception { + RevocationRequest(String caName, String id, String serial, String aki, String reason) throws Exception { if (isNullOrEmpty(id)) { if (isNullOrEmpty(serial) || isNullOrEmpty(aki)) { throw new Exception("Enrollment ID is empty, thus both aki and serial must have non-empty values"); @@ -50,7 +52,13 @@ class RevocationRequest { this.serial = serial; this.aki = aki; this.reason = reason; - this.caName = caNmae; + this.caName = caName; + } + + // Constructor with genCRL parameter + RevocationRequest(String caName, String id, String serial, String aki, String reason, Boolean genCRL) throws Exception { + this(caName, id, serial, aki, reason); + this.genCRL = genCRL; } String getUser() { @@ -85,6 +93,14 @@ void setReason(String reason) { this.reason = reason; } + Boolean getGenCRL() { + return genCRL; + } + + void setGenCRL(Boolean genCRL) { + this.genCRL = genCRL; + } + // Convert the revocation request to a JSON string String toJson() { StringWriter stringWriter = new StringWriter(); @@ -113,7 +129,10 @@ private JsonObject toJsonObject() { if (caName != null) { factory.add(HFCAClient.FABRIC_CA_REQPROP, caName); } - factory.add("reason", reason); + + if (genCRL != null) { + factory.add("gencrl", genCRL); + } return factory.build(); } } diff --git a/src/main/java/org/hyperledger/fabric_ca/sdk/exception/RevocationException.java b/src/main/java/org/hyperledger/fabric_ca/sdk/exception/RevocationException.java index 0e7bbac2..3bde5454 100644 --- a/src/main/java/org/hyperledger/fabric_ca/sdk/exception/RevocationException.java +++ b/src/main/java/org/hyperledger/fabric_ca/sdk/exception/RevocationException.java @@ -20,4 +20,8 @@ public RevocationException(String message, Exception parent) { super(message, parent); } + public RevocationException(String message) { + super(message); + } + } diff --git a/src/test/java/org/hyperledger/fabric_ca/sdk/RevocationRequestTest.java b/src/test/java/org/hyperledger/fabric_ca/sdk/RevocationRequestTest.java index 4e31ca51..77ba0323 100644 --- a/src/test/java/org/hyperledger/fabric_ca/sdk/RevocationRequestTest.java +++ b/src/test/java/org/hyperledger/fabric_ca/sdk/RevocationRequestTest.java @@ -23,17 +23,19 @@ public class RevocationRequestTest { private static final String revSerialNmbr = "987654321"; private static final String revAKI = "123456789"; private static final String revReason = "compromised"; + private static final Boolean revGenCRL = true; @Test public void testNewInstance() { try { RevocationRequest testRevocationReq = new RevocationRequest(revCAName, revEnrollmentID, revSerialNmbr, - revAKI, revReason); + revAKI, revReason, revGenCRL); Assert.assertEquals(testRevocationReq.getUser(), revEnrollmentID); Assert.assertEquals(testRevocationReq.getSerial(), revSerialNmbr); Assert.assertEquals(testRevocationReq.getAki(), revAKI); Assert.assertEquals(testRevocationReq.getReason(), revReason); + Assert.assertEquals(testRevocationReq.getGenCRL(), revGenCRL); } catch (Exception e) { Assert.fail("Unexpected Exception " + e.getMessage()); diff --git a/src/test/java/org/hyperledger/fabric_ca/sdkintegration/HFCAClientIT.java b/src/test/java/org/hyperledger/fabric_ca/sdkintegration/HFCAClientIT.java index b1f4092f..0a392ba8 100644 --- a/src/test/java/org/hyperledger/fabric_ca/sdkintegration/HFCAClientIT.java +++ b/src/test/java/org/hyperledger/fabric_ca/sdkintegration/HFCAClientIT.java @@ -55,6 +55,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -354,10 +355,146 @@ public void testUserRevoke() throws Exception { client.reenroll(user); } + // Tests attempting to revoke a user with Null reason + @Test + public void testUserRevokeNullReason() throws Exception { + + thrown.expect(EnrollmentException.class); + thrown.expectMessage("Failed to re-enroll user"); + + Calendar calendar = Calendar.getInstance(); // gets a calendar using the default time zone and locale. + calendar.add(Calendar.SECOND, -1); + Date revokedTinyBitAgoTime = calendar.getTime(); //avoid any clock skewing. + + SampleUser user = getTestUser(TEST_USER1_ORG); + + if (!user.isRegistered()) { + RegistrationRequest rr = new RegistrationRequest(user.getName(), TEST_USER1_AFFILIATION); + String password = "testUserRevoke"; + rr.setSecret(password); + rr.addAttribute(new Attribute("user.role", "department lead")); + rr.addAttribute(new Attribute("hf.revoker", "true")); + user.setEnrollmentSecret(client.register(rr, admin)); // Admin can register other users. + if (!user.getEnrollmentSecret().equals(password)) { + fail("Secret returned from RegistrationRequest not match : " + user.getEnrollmentSecret()); + } + } + + sleepALittle(); + + if (!user.isEnrolled()) { + EnrollmentRequest req = new EnrollmentRequest("profile 2", "label 2", null); + req.addHost("example3.ibm.com"); + user.setEnrollment(client.enroll(user.getName(), user.getEnrollmentSecret(), req)); + + // verify + String cert = user.getEnrollment().getCert(); + verifyOptions(cert, req); + } + + sleepALittle(); + + int startedWithRevokes = -1; + + if (!testConfig.isRunningAgainstFabric10()) { + + startedWithRevokes = getRevokes(null).length; //one more after we do this revoke. + } + + // revoke all enrollment of this user + client.revoke(admin, user.getName(), null); + if (!testConfig.isRunningAgainstFabric10()) { + final int newRevokes = getRevokes(null).length; + + assertEquals(format("Expected one more revocation %d, but got %d", startedWithRevokes + 1, newRevokes), startedWithRevokes + 1, newRevokes); + } + + // trying to reenroll the revoked user should fail with an EnrollmentException + client.reenroll(user); + } + + // Tests revoking a user with genCRL using the revoke API + @Test + public void testUserRevokeGenCRL() throws Exception { + + if (testConfig.isRunningAgainstFabric10()) { + return; // needs v1.1 + } + + thrown.expect(EnrollmentException.class); + thrown.expectMessage("Failed to re-enroll user"); + + Calendar calendar = Calendar.getInstance(); // gets a calendar using the default time zone and locale. + calendar.add(Calendar.SECOND, -1); + Date revokedTinyBitAgoTime = calendar.getTime(); //avoid any clock skewing. + + SampleUser user1 = getTestUser(TEST_USER1_ORG); + SampleUser user2 = getTestUser(TEST_USER1_ORG); + + SampleUser[] users = new SampleUser[]{user1, user2}; + + for (SampleUser user : users) { + if (!user.isRegistered()) { + RegistrationRequest rr = new RegistrationRequest(user.getName(), TEST_USER1_AFFILIATION); + String password = "testUserRevoke"; + rr.setSecret(password); + rr.addAttribute(new Attribute("user.role", "department lead")); + rr.addAttribute(new Attribute("hf.revoker", "true")); + user.setEnrollmentSecret(client.register(rr, admin)); // Admin can register other users. + if (!user.getEnrollmentSecret().equals(password)) { + fail("Secret returned from RegistrationRequest not match : " + user.getEnrollmentSecret()); + } + } + + sleepALittle(); + + if (!user.isEnrolled()) { + EnrollmentRequest req = new EnrollmentRequest("profile 2", "label 2", null); + req.addHost("example3.ibm.com"); + user.setEnrollment(client.enroll(user.getName(), user.getEnrollmentSecret(), req)); + + // verify + String cert = user.getEnrollment().getCert(); + verifyOptions(cert, req); + } + } + + sleepALittle(); + + int startedWithRevokes = -1; + + startedWithRevokes = getRevokes(null).length; //one more after we do this revoke. + + // revoke all enrollment of this user and request back a CRL + String crl = client.revoke(admin, user1.getName(), null, true); + assertNotNull("Failed to get CRL using the Revoke API", crl); + + final int newRevokes = getRevokes(null).length; + + assertEquals(format("Expected one more revocation %d, but got %d", startedWithRevokes + 1, newRevokes), startedWithRevokes + 1, newRevokes); + + final int crlLength = parseCRL(crl).length; + + assertEquals(format("The number of revokes %d does not equal the number of revoked certificates (%d) in crl", newRevokes, crlLength), newRevokes, crlLength); + + // trying to reenroll the revoked user should fail with an EnrollmentException + client.reenroll(user1); + + String crl2 = client.revoke(admin, user2.getName(), null, false); + assertEquals("CRL not requested, CRL should be empty", "", crl2); + + } + + TBSCertList.CRLEntry[] getRevokes(Date r) throws Exception { String crl = client.generateCRL(admin, r, null, null, null); + return parseCRL(crl); + } + + TBSCertList.CRLEntry[] parseCRL(String crl) throws Exception { + Base64.Decoder b64dec = Base64.getDecoder(); final byte[] decode = b64dec.decode(crl.getBytes(UTF_8)); @@ -433,17 +570,6 @@ public void testEnrollUnknownClient() throws Exception { clientWithName.enroll(admin.getName(), TEST_ADMIN_PW); } - // revoke1: revoke(User revoker, Enrollment enrollment, String reason) - @Test - public void testRevoke1NullReason() throws Exception { - - thrown.expect(RevocationException.class); - thrown.expectMessage("cannot be null"); - - SampleUser user = getEnrolledUser(TEST_ADMIN_ORG); - client.revoke(admin, user.getEnrollment(), null); - } - // revoke2: revoke(User revoker, String revokee, String reason) @Test public void testRevoke2UnknownUser() throws Exception { @@ -454,16 +580,6 @@ public void testRevoke2UnknownUser() throws Exception { client.revoke(admin, "unknownUser", "remove user2"); } - @Test - public void testRevoke2NullReason() throws Exception { - - thrown.expect(RevocationException.class); - thrown.expectMessage("cannot be null"); - - SampleUser user = getEnrolledUser(TEST_ADMIN_ORG); - client.revoke(admin, user.getName(), null); - } - @Test public void testMockEnrollSuccessFalse() throws Exception {