mirror of
https://github.com/corda/corda.git
synced 2025-04-19 08:36:39 +00:00
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:
parent
775877d173
commit
5f49bfc88a
@ -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
|
||||
}
|
@ -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)) {
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
@ -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()
|
||||
)
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
@ -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>
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user