CRL - persistence layer (#509)

* Addressing review comments

* Certificate Revocation List - persistence layer

* Addressing review comments

* Addressing review comments

* Addressing review comments

* Adding a crl test
This commit is contained in:
Michal Kit 2018-03-08 13:28:02 +00:00 committed by GitHub
parent 775877d173
commit 5f49bfc88a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 293 additions and 4 deletions

View File

@ -0,0 +1,36 @@
package com.r3.corda.networkmanage.common.persistence
import java.security.cert.X509CRL
import java.time.Instant
/**
* Interface for managing certificate revocation list persistence
*/
interface CertificateRevocationListStorage {
/**
* Retrieves the latest certificate revocation list.
*
* @param crlIssuer CRL issuer CA type.
* @return latest revocation list.
*/
fun getCertificateRevocationList(crlIssuer: CrlIssuer): X509CRL
/**
* Persists a new revocation list. Upon saving, statuses
* of the approved revocation requests will automatically change to [RequestStatus.DONE].
*
* @param crl signed instance of the certificate revocation list. It will be serialized and stored as part of a
* database entity.
* @param crlIssuer CRL issuer CA type.
* @param signedBy who signed this CRL.
* @param revokedAt revocation time.
*/
fun saveCertificateRevocationList(crl: X509CRL, crlIssuer: CrlIssuer, signedBy: String, revokedAt: Instant)
}
/**
* There are 2 CAs that issue CRLs (i.e. Root CA and Doorman CA).
*/
enum class CrlIssuer {
ROOT, DOORMAN
}

View File

@ -59,6 +59,7 @@ sealed class NetworkManagementSchemaServices {
CertificateSigningRequestEntity::class.java,
CertificateDataEntity::class.java,
CertificateRevocationRequestEntity::class.java,
CertificateRevocationListEntity::class.java,
NodeInfoEntity::class.java,
NetworkParametersEntity::class.java,
NetworkMapEntity::class.java)) {

View File

@ -0,0 +1,58 @@
package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity
import com.r3.corda.networkmanage.common.persistence.entity.CertificateRevocationListEntity
import com.r3.corda.networkmanage.common.persistence.entity.CertificateRevocationRequestEntity
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import java.math.BigInteger
import java.security.cert.X509CRL
import java.time.Instant
class PersistentCertificateRevocationListStorage(private val database: CordaPersistence) : CertificateRevocationListStorage {
override fun getCertificateRevocationList(crlIssuer: CrlIssuer): X509CRL {
return database.transaction {
val builder = session.criteriaBuilder
val query = builder.createQuery(CertificateRevocationListEntity::class.java).run {
from(CertificateRevocationListEntity::class.java).run {
orderBy(builder.desc(get<String>(CertificateRevocationListEntity::id.name)))
where(builder.equal(get<CrlIssuer>(CertificateRevocationListEntity::crlIssuer.name), crlIssuer))
}
}
// We just want the last signed entry
X509CertificateFactory().delegate.generateCRL(session.createQuery(query).setMaxResults(1).singleResult.crlBytes.inputStream()) as X509CRL
}
}
override fun saveCertificateRevocationList(crl: X509CRL, crlIssuer: CrlIssuer, signedBy: String, revokedAt: Instant) {
database.transaction {
crl.revokedCertificates.forEach {
revokeCertificate(it.serialNumber, revokedAt, this)
}
session.save(CertificateRevocationListEntity(
crlBytes = crl.encoded,
crlIssuer = crlIssuer,
signedBy = signedBy,
modifiedAt = Instant.now()
))
}
}
private fun revokeCertificate(certificateSerialNumber: BigInteger, time: Instant, transaction: DatabaseTransaction) {
val revocation = transaction.singleRequestWhere(CertificateRevocationRequestEntity::class.java) { builder, path ->
builder.equal(path.get<BigInteger>(CertificateRevocationRequestEntity::certificateSerialNumber.name), certificateSerialNumber)
}
revocation ?: throw IllegalStateException("The certificate revocation request for $certificateSerialNumber does not exist")
check(revocation.status in arrayOf(RequestStatus.APPROVED, RequestStatus.DONE)) {
"The certificate revocation request for $certificateSerialNumber has unexpected status of ${revocation.status}"
}
val session = transaction.session
val certificateData = session.merge(revocation.certificateData.copy(certificateStatus = CertificateStatus.REVOKED)) as CertificateDataEntity
session.merge(revocation.copy(
status = RequestStatus.DONE,
modifiedAt = time,
certificateData = certificateData
))
}
}

View File

@ -0,0 +1,29 @@
package com.r3.corda.networkmanage.common.persistence.entity
import com.r3.corda.networkmanage.common.persistence.CrlIssuer
import org.hibernate.envers.Audited
import java.time.Instant
import javax.persistence.*
@Entity
@Table(name = "certificate_revocation_list")
class CertificateRevocationListEntity(
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
val id: Long? = null,
@Column(name = "issuer")
val crlIssuer: CrlIssuer,
@Lob
@Column(name = "crl_bytes", nullable = false)
val crlBytes: ByteArray,
@Audited
@Column(name = "signed_by", length = 512)
val signedBy: String,
@Audited
@Column(name = "modified_at", nullable = false)
val modifiedAt: Instant = Instant.now()
)

View File

@ -52,8 +52,7 @@ class CertificateRevocationRequestEntity(
@Column(name = "remark", length = 256)
val remark: String? = null
) {
fun copy(id: Long? = this.id,
requestId: String = this.requestId,
fun copy(requestId: String = this.requestId,
certificateData: CertificateDataEntity = this.certificateData,
certificateSerialNumber: BigInteger = this.certificateSerialNumber,
status: RequestStatus = this.status,
@ -64,7 +63,7 @@ class CertificateRevocationRequestEntity(
revocationReason: CRLReason = this.revocationReason,
remark: String? = this.remark): CertificateRevocationRequestEntity {
return CertificateRevocationRequestEntity(
id = id,
id = this.id,
requestId = requestId,
certificateData = certificateData,
certificateSerialNumber = certificateSerialNumber,

View File

@ -132,5 +132,17 @@ class CertificateDataEntity(
return (toCertificatePath().certificates.first() as X509Certificate).subjectX500Principal.name
}
fun copy(certificateStatus: CertificateStatus = this.certificateStatus,
certificatePathBytes: ByteArray = this.certificatePathBytes,
certificateSigningRequest: CertificateSigningRequestEntity = this.certificateSigningRequest,
certificateSerialNumber: BigInteger = this.certificateSerialNumber): CertificateDataEntity {
return CertificateDataEntity(
id = this.id,
certificateStatus = certificateStatus,
certificatePathBytes = certificatePathBytes,
certificateSigningRequest = certificateSigningRequest,
certificateSerialNumber = certificateSerialNumber)
}
private fun toCertificatePath(): CertPath = buildCertPath(certificatePathBytes)
}

View File

@ -158,13 +158,43 @@
<constraints nullable="false"/>
</column>
<column name="revtype" type="TINYINT"/>
<column name="revocation_reason" type="NVARCHAR(255)"/>
<column name="revocation_reason" type="NVARCHAR(256)"/>
<column name="modified_at" type="TIMESTAMP"/>
<column name="modified_by" type="NVARCHAR(256)"/>
<column name="remark" type="NVARCHAR(256)"/>
<column name="status" type="NVARCHAR(256)"/>
</createTable>
</changeSet>
<changeSet author="R3.Corda" id="CRL">
<createTable tableName="certificate_revocation_list">
<column name="id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="issuer" type="NVARCHAR(256)">
<constraints nullable="false"/>
</column>
<column name="crl_bytes" type="BLOB"/>
<column name="modified_at" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="signed_by" type="NVARCHAR(512)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="R3.Corda" id="CRL_AUD">
<createTable tableName="certificate_revocation_list_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="modified_at" type="TIMESTAMP"/>
<column name="signed_by" type="NVARCHAR(512)"/>
</createTable>
</changeSet>
<changeSet author="R3.Corda" id="1520338500424-9">
<addPrimaryKey columnNames="hash" constraintName="PK_NP_H" tableName="network_parameters"/>
</changeSet>
@ -189,6 +219,12 @@
<changeSet author="R3.Corda" id="PK_CRR_AUD">
<addPrimaryKey columnNames="id, rev" constraintName="certificate_revocation_request_AUD_pk" tableName="certificate_revocation_request_AUD"/>
</changeSet>
<changeSet author="R3.Corda" id="PK_CRL">
<addPrimaryKey columnNames="id" constraintName="certificate_revocation_list_pk" tableName="certificate_revocation_list"/>
</changeSet>
<changeSet author="R3.Corda" id="PK_CRL_AUD">
<addPrimaryKey columnNames="id, rev" constraintName="certificate_revocation_list_AUD_pk" tableName="certificate_revocation_list_AUD"/>
</changeSet>
<changeSet author="R3.Corda" id="1520338500424-15">
<addUniqueConstraint columnNames="certificate_signing_request" constraintName="UK_CD_CSR" tableName="certificate_data"/>
</changeSet>

View File

@ -0,0 +1,118 @@
package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.TestBase
import net.corda.core.utilities.minutes
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.internal.DEV_INTERMEDIATE_CA
import net.corda.testing.node.MockServices
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.cert.jcajce.JcaX509CRLConverter
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils
import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.math.BigInteger
import java.security.cert.CRLReason
import java.security.cert.X509CRL
import java.time.Instant
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class PersistentCertificateRevocationListStorageTest : TestBase() {
private lateinit var crrStorage: PersistentCertificateRevocationRequestStorage
private lateinit var csrStorage: PersistentCertificateSigningRequestStorage
private lateinit var crlStorage: PersistentCertificateRevocationListStorage
private lateinit var persistence: CordaPersistence
companion object {
const val REPORTER = "TestReporter"
const val SIGNATURE_ALGORITHM = "SHA256withECDSA"
val REVOCATION_REASON = CRLReason.KEY_COMPROMISE
}
@Before
fun startDb() {
persistence = configureDatabase(MockServices.makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
crrStorage = PersistentCertificateRevocationRequestStorage(persistence)
csrStorage = PersistentCertificateSigningRequestStorage(persistence)
crlStorage = PersistentCertificateRevocationListStorage(persistence)
}
@After
fun closeDb() {
persistence.close()
}
@Test
fun `Saving CRL persists it in the DB and changes the status of the certificate revocation requests to DONE`() {
// given
val certificate = createNodeCertificate(csrStorage)
val requestId = crrStorage.saveRevocationRequest(certificate.serialNumber, REVOCATION_REASON, REPORTER)
crrStorage.approveRevocationRequest(requestId, "Approver")
val revocationRequest = crrStorage.getRevocationRequest(requestId)!!
val crl = createDummyCertificateRevocationList(listOf(revocationRequest.certificateSerialNumber))
// when
crlStorage.saveCertificateRevocationList(crl, CrlIssuer.DOORMAN, "TestSigner", Instant.now())
// then
assertNotNull(crlStorage.getCertificateRevocationList(CrlIssuer.DOORMAN)).apply {
assertEquals(crl, this)
}
assertNotNull(crrStorage.getRevocationRequest(requestId)).apply {
assertEquals(RequestStatus.DONE, status)
}
}
@Test
fun `Saving CRL does not change the status of other requests`() {
// given
val done = crrStorage.saveRevocationRequest(createNodeCertificate(csrStorage, legalName = "Bank A").serialNumber, REVOCATION_REASON, REPORTER)
crrStorage.approveRevocationRequest(done, "Approver")
val doneRevocationRequest = crrStorage.getRevocationRequest(done)!!
val new = crrStorage.saveRevocationRequest(createNodeCertificate(csrStorage, legalName = "Bank B").serialNumber, REVOCATION_REASON, REPORTER)
val crl = createDummyCertificateRevocationList(listOf(doneRevocationRequest.certificateSerialNumber))
crlStorage.saveCertificateRevocationList(crl, CrlIssuer.DOORMAN, "TestSigner", Instant.now())
val approved = crrStorage.saveRevocationRequest(createNodeCertificate(csrStorage, legalName = "Bank C").serialNumber, REVOCATION_REASON, REPORTER)
crrStorage.approveRevocationRequest(approved, "Approver")
val approvedRevocationRequest = crrStorage.getRevocationRequest(approved)!!
val newCrl = createDummyCertificateRevocationList(listOf(doneRevocationRequest.certificateSerialNumber, approvedRevocationRequest.certificateSerialNumber))
// when
crlStorage.saveCertificateRevocationList(newCrl, CrlIssuer.DOORMAN, "TestSigner", Instant.now())
// then
assertNotNull(crrStorage.getRevocationRequest(done)).apply {
assertEquals(RequestStatus.DONE, status)
}
assertNotNull(crrStorage.getRevocationRequest(new)).apply {
assertEquals(RequestStatus.NEW, status)
}
}
private fun createDummyCertificateRevocationList(serialNumbers: List<BigInteger> = emptyList()): X509CRL {
val (doormanCert, doormanKeys) = DEV_INTERMEDIATE_CA
val builder = JcaX509v2CRLBuilder(doormanCert.subjectX500Principal, Date())
val extensionUtils = JcaX509ExtensionUtils()
builder.addExtension(Extension.authorityKeyIdentifier, false, extensionUtils.createAuthorityKeyIdentifier(doormanCert))
val issuingDistPointName = GeneralName(
GeneralName.uniformResourceIdentifier, "http://dummy.com")
// This is required and needs to match the certificate settings with respect to being indirect
val issuingDistPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistPointName)), false, false)
builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistPoint)
builder.setNextUpdate(Date(System.currentTimeMillis() + 10.minutes.toMillis()))
serialNumbers.forEach {
builder.addCRLEntry(it, Date(System.currentTimeMillis() - 10.minutes.toMillis()), ReasonFlags.certificateHold)
}
val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(doormanKeys.private)
return JcaX509CRLConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCRL(builder.build(signer))
}
}