diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificateRevocationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificateRevocationRequestStorage.kt new file mode 100644 index 0000000000..fc87be0ebe --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificateRevocationRequestStorage.kt @@ -0,0 +1,69 @@ +package com.r3.corda.networkmanage.common.persistence + +import java.math.BigInteger +import java.security.cert.CRLReason +import java.time.Instant + +data class CertificateRevocationRequestData(val requestId: String, // This is a uniquely generated string + val certificateSerialNumber: BigInteger, + val revocationTime: Instant?, + val legalName: String, + val status: RequestStatus, + val reason: CRLReason, + val reporter: String) // Username of the reporter + +/** + * Interface for managing certificate revocation requests persistence + */ +interface CertificateRevocationRequestStorage { + + /** + * Creates a new revocation request for the given [certificateSerialNumber]. + * The newly created revocation request has the [RequestStatus.NEW] status. + * If the revocation request with the [certificateSerialNumber] already exists and has status + * [RequestStatus.NEW], [RequestStatus.APPROVED] or [RequestStatus.REVOKED] + * then nothing is persisted and the existing revocation request identifier is returned. + * + * @param certificateSerialNumber serial number of the certificate to be revoked. + * @param reason reason for revocation. See [java.security.cert.CRLReason] + * @param reporter who is requesting this revocation + * + * @return identifier of the newly created (or existing) revocation request. + */ + fun saveRevocationRequest(certificateSerialNumber: BigInteger, reason: CRLReason, reporter: String): String + + /** + * Retrieves the revocation request with the given [requestId] + * + * @param requestId revocation request identifier + * + * @return CertificateRevocationRequest matching the specified identifier. Or null if it doesn't exist. + */ + fun getRevocationRequest(requestId: String): CertificateRevocationRequestData? + + /** + * Retrieves all the revocation requests with the specified revocation request status. + * + * @param revocationStatus revocation request status of the returned revocation requests. + * + * @return list of certificate revocation requests that match the revocation request status. + */ + fun getRevocationRequests(revocationStatus: RequestStatus): List + + /** + * Changes the revocation request status to [RequestStatus.APPROVED]. + * + * @param requestId revocation request identifier + * @param approvedBy who is approving it + */ + fun approveRevocationRequest(requestId: String, approvedBy: String) + + /** + * Changes the revocation request status to [RequestStatus.REJECTED]. + * + * @param requestId revocation request identifier + * @param rejectedBy who is rejecting it + * @param reason description of the reason of this rejection. + */ + fun rejectRevocationRequest(requestId: String, rejectedBy: String, reason: String) +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificateSigningRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificateSigningRequestStorage.kt index e072254d89..510a26675b 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificateSigningRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificateSigningRequestStorage.kt @@ -76,10 +76,11 @@ interface CertificateSigningRequestStorage { /** * Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.DONE]. * @param requestId id of the certificate signing request + * @param certPath chain of certificates starting with the one generated in response to the CSR up to the root. * @param signedBy authority (its identifier) signing this request. * @throws IllegalArgumentException if request is not found or not in Approved state. */ - fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: String) + fun putCertificatePath(requestId: String, certPath: CertPath, signedBy: String) } sealed class CertificateResponse { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceUtils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceUtils.kt index 4c4278c60f..72ded980d7 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceUtils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistenceUtils.kt @@ -58,6 +58,7 @@ sealed class NetworkManagementSchemaServices { mappedTypes = listOf( CertificateSigningRequestEntity::class.java, CertificateDataEntity::class.java, + CertificateRevocationRequestEntity::class.java, NodeInfoEntity::class.java, NetworkParametersEntity::class.java, NetworkMapEntity::class.java)) { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt new file mode 100644 index 0000000000..1590aa1ab8 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt @@ -0,0 +1,108 @@ +package com.r3.corda.networkmanage.common.persistence + +import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity +import com.r3.corda.networkmanage.common.persistence.entity.CertificateRevocationRequestEntity +import net.corda.core.crypto.SecureHash +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel +import java.math.BigInteger +import java.security.cert.CRLReason +import java.time.Instant + +class PersistentCertificateRevocationRequestStorage(private val database: CordaPersistence) : CertificateRevocationRequestStorage { + override fun saveRevocationRequest(certificateSerialNumber: BigInteger, reason: CRLReason, reporter: String): String { + return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { + // Check if there is an entry for the given certificate serial number + val revocation = singleRequestWhere(CertificateRevocationRequestEntity::class.java) { builder, path -> + val serialNumberEqual = builder.equal(path.get(CertificateRevocationRequestEntity::certificateSerialNumber.name), certificateSerialNumber) + val statusNotEqualRejected = builder.notEqual(path.get(CertificateRevocationRequestEntity::status.name), RequestStatus.REJECTED) + builder.and(serialNumberEqual, statusNotEqualRejected) + } + if (revocation != null) { + revocation.requestId + } else { + val certificateData = singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> + val serialNumberEqual = builder.equal(path.get(CertificateDataEntity::certificateSerialNumber.name), certificateSerialNumber) + val statusEqualValid = builder.equal(path.get(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID) + builder.and(serialNumberEqual, statusEqualValid) + } + requireNotNull(certificateData) { "The certificate with the given serial number cannot be found." } + val requestId = SecureHash.randomSHA256().toString() + session.save(CertificateRevocationRequestEntity( + certificateSerialNumber = certificateSerialNumber, + revocationReason = reason, + requestId = requestId, + modifiedBy = reporter, + certificateData = certificateData!!, + reporter = reporter, + legalName = certificateData.legalName() + )) + requestId + } + } + } + + override fun getRevocationRequest(requestId: String): CertificateRevocationRequestData? = database.transaction { + getRevocationRequestEntity(requestId)?.toCertificateRevocationRequest() + } + + override fun getRevocationRequests(revocationStatus: RequestStatus): List { + return database.transaction { + val builder = session.criteriaBuilder + val query = builder.createQuery(CertificateRevocationRequestEntity::class.java).run { + from(CertificateRevocationRequestEntity::class.java).run { + where(builder.equal(get(CertificateRevocationRequestEntity::status.name), revocationStatus)) + } + } + session.createQuery(query).resultList.map { it.toCertificateRevocationRequest() } + } + } + + override fun approveRevocationRequest(requestId: String, approvedBy: String) { + database.transaction { + val revocation = getRevocationRequestEntity(requestId) + if (revocation == null) { + throw NoSuchElementException("Error while approving! Certificate revocation id=$id does not exist") + } else { + session.merge(revocation.copy( + status = RequestStatus.APPROVED, + modifiedAt = Instant.now(), + modifiedBy = approvedBy + )) + } + } + } + + override fun rejectRevocationRequest(requestId: String, rejectedBy: String, reason: String) { + database.transaction { + val revocation = getRevocationRequestEntity(requestId) + if (revocation == null) { + throw NoSuchElementException("Error while rejecting! Certificate revocation id=$id does not exist") + } else { + session.merge(revocation.copy( + status = RequestStatus.REJECTED, + modifiedAt = Instant.now(), + modifiedBy = rejectedBy, + remark = reason + )) + } + } + } + + private fun getRevocationRequestEntity(requestId: String): CertificateRevocationRequestEntity? = database.transaction { + singleRequestWhere(CertificateRevocationRequestEntity::class.java) { builder, path -> + builder.equal(path.get(CertificateRevocationRequestEntity::requestId.name), requestId) + } + } + + private fun CertificateRevocationRequestEntity.toCertificateRevocationRequest(): CertificateRevocationRequestData { + return CertificateRevocationRequestData( + requestId, + certificateSerialNumber, + if (status == RequestStatus.DONE) modifiedAt else null, + legalName, + status, + revocationReason, + reporter) + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt index f45d64b0d3..b4873ec914 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt @@ -18,6 +18,7 @@ import net.corda.core.crypto.Crypto.toSupportedPublicKey import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.internal.CertRole +import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel @@ -35,7 +36,7 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers private val allowedCertRoles = setOf(CertRole.NODE_CA, CertRole.SERVICE_IDENTITY) } - override fun putCertificatePath(requestId: String, certificates: CertPath, signedBy: String) { + override fun putCertificatePath(requestId: String, certPath: CertPath, signedBy: String) { return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { val request = singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> val requestIdEq = builder.equal(path.get(CertificateSigningRequestEntity::requestId.name), requestId) @@ -50,8 +51,9 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers session.merge(certificateSigningRequest) val certificateDataEntity = CertificateDataEntity( certificateStatus = CertificateStatus.VALID, - certificatePathBytes = certificates.encoded, - certificateSigningRequest = certificateSigningRequest) + certificatePathBytes = certPath.encoded, + certificateSigningRequest = certificateSigningRequest, + certificateSerialNumber = certPath.x509Certificates.first().serialNumber) session.persist(certificateDataEntity) } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateRevocationRequestEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateRevocationRequestEntity.kt new file mode 100644 index 0000000000..dd29f19fcb --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateRevocationRequestEntity.kt @@ -0,0 +1,80 @@ +package com.r3.corda.networkmanage.common.persistence.entity + +import com.r3.corda.networkmanage.common.persistence.RequestStatus +import org.hibernate.envers.Audited +import java.math.BigInteger +import java.security.cert.CRLReason +import java.time.Instant +import javax.persistence.* + +@Entity +@Table(name = "certificate_revocation_request") +class CertificateRevocationRequestEntity( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + val id: Long? = null, + + @Column(name = "request_id", length = 256, nullable = false, unique = true) + val requestId: String, + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "certificate_data") + val certificateData: CertificateDataEntity, + + @Column(name = "certificate_serial_number", nullable = false) + val certificateSerialNumber: BigInteger, + + @Column(name = "legal_name", length = 256, nullable = false) + val legalName: String, + + @Audited + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + val status: RequestStatus = RequestStatus.NEW, + + @Column(name = "reporter", nullable = false, length = 512) + val reporter: String, + + @Audited + @Column(name = "modified_by", nullable = false, length = 512) + val modifiedBy: String, + + @Audited + @Column(name = "modified_at", nullable = false) + val modifiedAt: Instant = Instant.now(), + + @Audited + @Column(name = "revocation_reason", nullable = false) + @Enumerated(EnumType.STRING) + val revocationReason: CRLReason, + + @Audited + @Column(name = "remark", length = 256) + val remark: String? = null +) { + fun copy(id: Long? = this.id, + requestId: String = this.requestId, + certificateData: CertificateDataEntity = this.certificateData, + certificateSerialNumber: BigInteger = this.certificateSerialNumber, + status: RequestStatus = this.status, + legalName: String = this.legalName, + reporter: String = this.reporter, + modifiedBy: String = this.modifiedBy, + modifiedAt: Instant = this.modifiedAt, + revocationReason: CRLReason = this.revocationReason, + remark: String? = this.remark): CertificateRevocationRequestEntity { + return CertificateRevocationRequestEntity( + id = id, + requestId = requestId, + certificateData = certificateData, + certificateSerialNumber = certificateSerialNumber, + status = status, + legalName = legalName, + reporter = reporter, + modifiedBy = modifiedBy, + modifiedAt = modifiedAt, + revocationReason = revocationReason, + remark = remark + ) + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt index af4cf9dca6..114803b92d 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt @@ -18,7 +18,9 @@ import com.r3.corda.networkmanage.common.utils.buildCertPath import net.corda.core.crypto.SecureHash import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.hibernate.envers.Audited +import java.math.BigInteger import java.security.cert.CertPath +import java.security.cert.X509Certificate import java.time.Instant import javax.persistence.* @@ -114,7 +116,10 @@ class CertificateDataEntity( @OneToOne(fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "certificate_signing_request", foreignKey = ForeignKey(name = "FK__cert_data__cert_sign_req")) - val certificateSigningRequest: CertificateSigningRequestEntity + val certificateSigningRequest: CertificateSigningRequestEntity, + + @Column(name = "certificate_serial_number", unique = true) + val certificateSerialNumber: BigInteger ) { fun toCertificateData(): CertificateData { return CertificateData( @@ -123,5 +128,9 @@ class CertificateDataEntity( ) } + fun legalName(): String { + return (toCertificatePath().certificates.first() as X509Certificate).subjectX500Principal.name + } + private fun toCertificatePath(): CertPath = buildCertPath(certificatePathBytes) } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/migration/CertificateDataSerialNumber.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/migration/CertificateDataSerialNumber.kt new file mode 100644 index 0000000000..6751840aa8 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/migration/CertificateDataSerialNumber.kt @@ -0,0 +1,49 @@ +package com.r3.corda.networkmanage.common.persistence.migration + +import com.r3.corda.networkmanage.common.utils.buildCertPath +import liquibase.change.custom.CustomTaskChange +import liquibase.database.Database +import liquibase.database.jvm.JdbcConnection +import liquibase.exception.ValidationErrors +import liquibase.resource.ResourceAccessor +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.math.BigDecimal +import java.security.Security +import java.security.cert.X509Certificate +import java.sql.ResultSet + +class CertificateDataSerialNumber : CustomTaskChange { + override fun validate(database: Database?): ValidationErrors? { + return null + } + + override fun setUp() { + // Do nothing + } + + override fun setFileOpener(resourceAccessor: ResourceAccessor?) { + // Do nothing + } + + override fun getConfirmationMessage(): String { + return "Certificate data serial numbers have been extracted and persisted." + } + + override fun execute(database: Database?) { + Security.addProvider(BouncyCastleProvider()) + val jdbcConnection = database!!.connection as JdbcConnection + jdbcConnection.autoCommit = true + val statement = jdbcConnection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE) + val resultSet = statement.executeQuery("SELECT certificate_path_bytes, certificate_serial_number FROM certificate_data") + while (resultSet.next()) { + val blob = resultSet.getBlob(1) + val certPath = buildCertPath(blob.getBytes(1, blob.length().toInt())) + blob.free() + val serialNumber = (certPath.certificates.first() as X509Certificate).serialNumber + resultSet.updateBigDecimal(2, BigDecimal(serialNumber)) + resultSet.updateRow() + } + statement.close() + } + +} \ No newline at end of file diff --git a/network-management/src/main/resources/migration/network-manager.changelog-adding-crr.xml b/network-management/src/main/resources/migration/network-manager.changelog-adding-crr.xml new file mode 100644 index 0000000000..928b67ffa6 --- /dev/null +++ b/network-management/src/main/resources/migration/network-manager.changelog-adding-crr.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/network-management/src/main/resources/migration/network-manager.changelog-master.xml b/network-management/src/main/resources/migration/network-manager.changelog-master.xml index c8c08f7230..7be4d3c3a9 100644 --- a/network-management/src/main/resources/migration/network-manager.changelog-master.xml +++ b/network-management/src/main/resources/migration/network-manager.changelog-master.xml @@ -15,5 +15,6 @@ + diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt index 5f2d5593c5..3a53b3da31 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt @@ -11,15 +11,21 @@ package com.r3.corda.networkmanage import com.nhaarman.mockito_kotlin.mock -import com.r3.corda.networkmanage.common.persistence.CertificateData -import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest -import com.r3.corda.networkmanage.common.persistence.CertificateStatus -import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.common.persistence.* import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.CertRole +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.internal.createDevNodeCaCertPath import org.bouncycastle.pkcs.PKCS10CertificationRequest +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.junit.Rule +import java.security.KeyPair import java.security.cert.CertPath +import java.security.cert.X509Certificate +import javax.security.auth.x500.X500Principal abstract class TestBase { @Rule @@ -55,4 +61,27 @@ abstract class TestBase { certPath = certPath ) } + + private fun generateSignedCertPath(csr: PKCS10CertificationRequest, keyPair: KeyPair): CertPath { + return JcaPKCS10CertificationRequest(csr).run { + val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)), keyPair) + X509Utilities.buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate) + } + } + + protected fun createNodeCertificate(csrStorage: CertificateSigningRequestStorage, legalName: String = "LegalName"): X509Certificate { + val (csr, nodeKeyPair) = createRequest(legalName, certRole = CertRole.NODE_CA) + // Add request to DB. + val requestId = csrStorage.saveRequest(csr) + csrStorage.markRequestTicketCreated(requestId) + csrStorage.approveRequest(requestId, "Approver") + val certPath = generateSignedCertPath(csr, nodeKeyPair) + csrStorage.putCertificatePath( + requestId, + certPath, + CertificateSigningRequestStorage.DOORMAN_SIGNATURE + ) + return certPath.x509Certificates.first() + } + } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt new file mode 100644 index 0000000000..141602b441 --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt @@ -0,0 +1,125 @@ +package com.r3.corda.networkmanage.common.persistence + +import com.r3.corda.networkmanage.TestBase +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseConfig +import net.corda.testing.node.MockServices +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.math.BigInteger +import java.security.cert.CRLReason +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull + +class PersistentCertificateRevocationRequestStorageTest : TestBase() { + private lateinit var crrStorage: PersistentCertificateRevocationRequestStorage + private lateinit var csrStorage: PersistentCertificateSigningRequestStorage + private lateinit var persistence: CordaPersistence + + companion object { + const val REPORTER = "TestReporter" + val REVOCATION_REASON = CRLReason.KEY_COMPROMISE + } + + @Before + fun startDb() { + persistence = configureDatabase(MockServices.makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)) + crrStorage = PersistentCertificateRevocationRequestStorage(persistence) + csrStorage = PersistentCertificateSigningRequestStorage(persistence) + } + + @After + fun closeDb() { + persistence.close() + } + + @Test + fun `Certificate revocation request is persisted correctly`() { + // given + val certificate = createNodeCertificate(csrStorage) + + // when + val requestId = crrStorage.saveRevocationRequest(certificate.serialNumber, REVOCATION_REASON, REPORTER) + + // then + assertNotNull(crrStorage.getRevocationRequest(requestId)).apply { + assertEquals(certificate.serialNumber, certificateSerialNumber) + assertEquals(REVOCATION_REASON, reason) + assertEquals(REPORTER, reporter) + } + } + + @Test + fun `Retrieving a certificate revocation request succeeds`() { + // given + val certificate = createNodeCertificate(csrStorage) + val requestId = crrStorage.saveRevocationRequest(certificate.serialNumber, REVOCATION_REASON, REPORTER) + + // when + val request = crrStorage.getRevocationRequest(requestId) + + // then + assertNotNull(request) + } + + @Test + fun `Retrieving a certificate revocation requests by status returns correct data`() { + // given + (1..10).forEach { + crrStorage.saveRevocationRequest(createNodeCertificate(csrStorage, "LegalName" + it.toString()).serialNumber, REVOCATION_REASON, REPORTER) + } + (11..15).forEach { + val requestId = crrStorage.saveRevocationRequest(createNodeCertificate(csrStorage, "LegalName" + it.toString()).serialNumber, REVOCATION_REASON, REPORTER) + crrStorage.approveRevocationRequest(requestId, "Approver") + } + + // when + val result = crrStorage.getRevocationRequests(RequestStatus.APPROVED) + + // then + assertEquals(5, result.size) + } + + @Test + fun `Certificate revocation request is not persisted if a valid certificate cannot be found`() { + // given + + // then + assertFailsWith(IllegalArgumentException::class) { + // when + crrStorage.saveRevocationRequest(BigInteger.TEN, REVOCATION_REASON, REPORTER) + } + } + + @Test + fun `Approving a certificate revocation request changes its status`() { + // given + val certificate = createNodeCertificate(csrStorage) + val requestId = crrStorage.saveRevocationRequest(certificate.serialNumber, REVOCATION_REASON, REPORTER) + + // when + crrStorage.approveRevocationRequest(requestId, "Approver") + + // then + assertNotNull(crrStorage.getRevocationRequest(requestId)).apply { + assertEquals(RequestStatus.APPROVED, status) + } + } + + @Test + fun `Rejecting a certificate revocation request changes its status`() { + // given + val certificate = createNodeCertificate(csrStorage) + val requestId = crrStorage.saveRevocationRequest(certificate.serialNumber, REVOCATION_REASON, REPORTER) + + // when + crrStorage.rejectRevocationRequest(requestId, "Rejector", "No reason") + + // then + assertNotNull(crrStorage.getRevocationRequest(requestId)).apply { + assertEquals(RequestStatus.REJECTED, status) + } + } +} \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorageTest.kt similarity index 100% rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorageTest.kt