mirror of
https://github.com/corda/corda.git
synced 2025-01-14 16:59:52 +00:00
Certificate Revocation Request - Persistence Layer (#507)
* Certificate Revocation Request - Persistence Layer * Addressing review comments * Addressing review comments * Adding comments to the requestId field
This commit is contained in:
parent
1ce4805c6d
commit
6bed95c02b
@ -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<CertificateRevocationRequestData>
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
@ -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 {
|
||||
|
@ -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)) {
|
||||
|
@ -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<BigInteger>(CertificateRevocationRequestEntity::certificateSerialNumber.name), certificateSerialNumber)
|
||||
val statusNotEqualRejected = builder.notEqual(path.get<RequestStatus>(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<BigInteger>(CertificateDataEntity::certificateSerialNumber.name), certificateSerialNumber)
|
||||
val statusEqualValid = builder.equal(path.get<CertificateStatus>(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<CertificateRevocationRequestData> {
|
||||
return database.transaction {
|
||||
val builder = session.criteriaBuilder
|
||||
val query = builder.createQuery(CertificateRevocationRequestEntity::class.java).run {
|
||||
from(CertificateRevocationRequestEntity::class.java).run {
|
||||
where(builder.equal(get<RequestStatus>(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<String>(CertificateRevocationRequestEntity::requestId.name), requestId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CertificateRevocationRequestEntity.toCertificateRevocationRequest(): CertificateRevocationRequestData {
|
||||
return CertificateRevocationRequestData(
|
||||
requestId,
|
||||
certificateSerialNumber,
|
||||
if (status == RequestStatus.DONE) modifiedAt else null,
|
||||
legalName,
|
||||
status,
|
||||
revocationReason,
|
||||
reporter)
|
||||
}
|
||||
}
|
@ -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<String>(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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
<changeSet id="Certificate Revocation Request" author="R3.Corda">
|
||||
<createTable tableName="certificate_revocation_request">
|
||||
<column name="id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="request_id" type="NVARCHAR(256)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
<column name="certificate_serial_number" type="NUMERIC(28)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="legal_name" type="NVARCHAR(256)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="status" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="reporter" type="NVARCHAR(512)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="modified_by" type="NVARCHAR(512)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="modified_at" type="TIMESTAMP">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="remark" type="NVARCHAR(256)"/>
|
||||
<column name="revocation_reason" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="certificate_data" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="certificate_revocation_request_AUD">
|
||||
<column name="id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="rev" type="INT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="revtype" type="TINYINT"/>
|
||||
<column name="revocation_reason" type="VARCHAR(255)"/>
|
||||
<column name="modified_at" type="TIMESTAMP"/>
|
||||
<column name="modified_by" type="NVARCHAR(256)"/>
|
||||
<column name="remark" type="NVARCHAR(256)"/>
|
||||
<column name="status" type="VARCHAR(255)"/>
|
||||
</createTable>
|
||||
<addColumn tableName="certificate_data">
|
||||
<column name="certificate_serial_number" type="NUMERIC(28)"/>
|
||||
</addColumn>
|
||||
<addPrimaryKey columnNames="id" constraintName="certificate_revocation_request_pk" tableName="certificate_revocation_request"/>
|
||||
<addPrimaryKey columnNames="id, rev" constraintName="certificate_revocation_request_AUD_pk" tableName="certificate_revocation_request_AUD"/>
|
||||
<createIndex indexName="certificate_revocation_request_AUD_index" tableName="certificate_revocation_request_AUD">
|
||||
<column name="rev"/>
|
||||
</createIndex>
|
||||
<addForeignKeyConstraint baseColumnNames="certificate_data"
|
||||
baseTableName="certificate_revocation_request"
|
||||
constraintName="cert_data__cert_rev_req_fk"
|
||||
referencedColumnNames="id"
|
||||
referencedTableName="certificate_data"/>
|
||||
<addForeignKeyConstraint baseColumnNames="rev"
|
||||
baseTableName="certificate_revocation_request_AUD"
|
||||
constraintName="cert_rev_req__REVINFO_AUD_fk"
|
||||
referencedColumnNames="rev"
|
||||
referencedTableName="REVINFO"/>
|
||||
</changeSet>
|
||||
<changeSet id="Certificate Serial Number" author="R3.Corda" runAlways="false" failOnError="true">
|
||||
<customChange class="com.r3.corda.networkmanage.common.persistence.migration.CertificateDataSerialNumber"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -15,5 +15,6 @@
|
||||
<include file="migration/network-manager.changelog-signing-network-params.xml"/>
|
||||
<include file="migration/network-manager.changelog-pub-key-move.xml"/>
|
||||
<include file="migration/network-manager.changelog-modified-by-refactor.xml"/>
|
||||
<include file="migration/network-manager.changelog-adding-crr.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user