mirror of
https://github.com/corda/corda.git
synced 2025-03-14 00:06:45 +00:00
Fixes for the CRL feature after the first round of testing (#837)
* Fixes for the CRL feature after the first round of testing * Addressing review comments * Synchronising changes with OS * Addressing review comments - round 2
This commit is contained in:
parent
1c575b5364
commit
da6957e6d1
@ -238,6 +238,13 @@ absolute path to the node's base directory.
|
||||
|
||||
.. note:: This is temporary feature for onboarding network participants that limits their visibility for privacy reasons.
|
||||
|
||||
:tlsCertCrlDistPoint: CRL distribution point (i.e. URL) for the TLS certificate. Default value is NULL, which indicates no CRL availability for the TLS certificate.
|
||||
Note: If crlCheckSoftFail is FALSE (meaning that there is the strict CRL checking mode) this value needs to be set.
|
||||
|
||||
:tlsCertCrlIssuer: CRL issuer (given in the X500 name format) for the TLS certificate. Default value is NULL,
|
||||
which indicates that the issuer of the TLS certificate is also the issuer of the CRL.
|
||||
Note: If this parameter is set then the tlsCertCrlDistPoint needs to be set as well.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
|
@ -39,6 +39,8 @@ Allowed parameters are:
|
||||
|
||||
:approveInterval: How often to process Jira approved requests in seconds.
|
||||
|
||||
:crlEndpoint: URL to the CRL issued by the Doorman CA. This parameter is only useful when Doorman is executing in the local signing mode.
|
||||
|
||||
:jira: The Jira configuration for certificate signing requests
|
||||
|
||||
:address: The URL to use to connect to Jira
|
||||
@ -66,6 +68,12 @@ Allowed parameters are:
|
||||
|
||||
:approveAll: Whether to approve all requests (defaults to false), this is for debug only.
|
||||
|
||||
:caCrlPath: Path (including the file name) to the location of the file containing the bytes of the CRL issued by the ROOT CA.
|
||||
Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER
|
||||
|
||||
:emptyCrlPath: Path (including the file name) to the location of the generated file containing the bytes of the empty CRL issued by the ROOT CA.
|
||||
Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER
|
||||
|
||||
:jira: The Jira configuration for certificate revocation requests
|
||||
|
||||
:address: The URL to use to connect to Jira
|
||||
@ -88,6 +96,22 @@ Allowed parameters are:
|
||||
|
||||
:rootStorePath: Path for the root keystore
|
||||
|
||||
:caCrlPath: Path (including the file name) to the location of the generated file containing the bytes of the CRL issued by the ROOT CA.
|
||||
This configuration parameter is used in the ROOT_KEYGEN mode.
|
||||
Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER
|
||||
|
||||
:caCrlUrl: URL to the CRL issued by the ROOT CA. This URL is going to be included in the generated CRL that is signed by the ROOT CA.
|
||||
This configuration parameter is used in the ROOT_KEYGEN and CA_KEYGEN modes.
|
||||
|
||||
:emptyCrlPath: Path (including the file name) to the location of the generated file containing the bytes of the empty CRL issued by the ROOT CA.
|
||||
This configuration parameter is used in the ROOT_KEYGEN mode.
|
||||
Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER
|
||||
This CRL is to allow nodes to operate in the strict CRL checking mode. This mode requires all the certificates in the chain being validated
|
||||
to point a CRL. Since the TLS-level certificate is managed by the nodes, this CRL is a facility one for infrastructures without CRL provisioning.
|
||||
|
||||
:emptyCrlUrl: URL to the empty CRL issued by the ROOT CA. This URL is going to be included in the generated empty CRL that is signed by the ROOT CA.
|
||||
This configuration parameter is used in the ROOT_KEYGEN mode.
|
||||
|
||||
Bootstrapping the network parameters
|
||||
------------------------------------
|
||||
When doorman is running it will serve the current network parameters. The first time doorman is
|
||||
|
@ -68,4 +68,5 @@ data class NotaryRegistrationConfig(val legalName: CordaX500Name,
|
||||
val networkRootTrustStorePassword: String?,
|
||||
val trustStorePassword: String?,
|
||||
val keystorePath: Path?,
|
||||
val crlCheckSoftFail: Boolean)
|
||||
val crlCheckSoftFail: Boolean,
|
||||
val crlDistributionPoint: URL? = null)
|
||||
|
@ -10,17 +10,26 @@
|
||||
|
||||
package com.r3.corda.networkmanage.common
|
||||
|
||||
import com.r3.corda.networkmanage.common.utils.createSignedCrl
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.testing.database.DatabaseConstants
|
||||
import net.corda.testing.node.internal.databaseProviderDataSourceConfig
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
|
||||
const val HOST = "localhost"
|
||||
|
||||
const val DOORMAN_DB_NAME = "doorman"
|
||||
|
||||
fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: String? = null) : Config {
|
||||
fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: String? = null): Config {
|
||||
val nodeName = nodeName ?: SecureHash.randomSHA256().toString()
|
||||
val h2InstanceName = if (postfix != null) nodeName + "_" + postfix else nodeName
|
||||
|
||||
@ -31,6 +40,21 @@ fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: Stri
|
||||
DatabaseConstants.DATA_SOURCE_PASSWORD to ""))
|
||||
}
|
||||
|
||||
fun generateEmptyCrls(tempFolder: TemporaryFolder, rootCertAndKeyPair: CertificateAndKeyPair, directEndpoint: URL, indirectEndpoint: URL): Pair<Path, Path> {
|
||||
val localSigner = LocalSigner(rootCertAndKeyPair)
|
||||
val directCrl = createSignedCrl(rootCertAndKeyPair.certificate, directEndpoint, 10.days, localSigner, emptyList(), false)
|
||||
val indirectCrl = createSignedCrl(rootCertAndKeyPair.certificate, indirectEndpoint, 10.days, localSigner, emptyList(), true)
|
||||
val directCrlFile = tempFolder.newFile()
|
||||
FileUtils.writeByteArrayToFile(directCrlFile, directCrl.encoded)
|
||||
val indirectCrlFile = tempFolder.newFile()
|
||||
FileUtils.writeByteArrayToFile(indirectCrlFile, indirectCrl.encoded)
|
||||
return Pair(directCrlFile.toPath(), indirectCrlFile.toPath())
|
||||
}
|
||||
|
||||
fun getCaCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/root")
|
||||
fun getEmptyCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/empty")
|
||||
fun getNodeCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/doorman")
|
||||
|
||||
//TODO add more dbs to test once doorman supports them
|
||||
fun configSupplierForSupportedDatabases(): (String?, String?) -> Config =
|
||||
when (System.getProperty("custom.databaseProvider", "")) {
|
||||
|
@ -10,9 +10,7 @@
|
||||
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.r3.corda.networkmanage.common.DOORMAN_DB_NAME
|
||||
import com.r3.corda.networkmanage.common.configSupplierForSupportedDatabases
|
||||
import com.r3.corda.networkmanage.common.networkMapInMemoryH2DataSourceConfig
|
||||
import com.r3.corda.networkmanage.common.*
|
||||
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import net.corda.cordform.CordformNode
|
||||
@ -44,7 +42,9 @@ import net.corda.testing.node.internal.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.internal.makeTestDatabaseProperties
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.*
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.security.cert.X509Certificate
|
||||
import kotlin.streams.toList
|
||||
|
||||
@ -78,17 +78,25 @@ class NodeRegistrationTest : IntegrationTest() {
|
||||
|
||||
private var server: NetworkManagementServer? = null
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis)
|
||||
private val revocationConfig: CertificateRevocationConfig
|
||||
get() = CertificateRevocationConfig(
|
||||
private lateinit var revocationConfig: CertificateRevocationConfig
|
||||
|
||||
private fun createCertificateRevocationConfig(emptyCrlPath: Path, caCrlPath: Path): CertificateRevocationConfig {
|
||||
return CertificateRevocationConfig(
|
||||
approveAll = true,
|
||||
jira = null,
|
||||
approveInterval = timeoutMillis,
|
||||
crlCacheTimeout = timeoutMillis,
|
||||
localSigning = CertificateRevocationConfig.LocalSigning(
|
||||
crlEndpoint = URL("http://test.com/crl"),
|
||||
crlUpdateInterval = timeoutMillis)
|
||||
)
|
||||
crlEndpoint = getNodeCrlEndpoint(serverAddress),
|
||||
crlUpdateInterval = timeoutMillis),
|
||||
emptyCrlPath = emptyCrlPath,
|
||||
caCrlPath = caCrlPath)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
@ -97,6 +105,8 @@ class NodeRegistrationTest : IntegrationTest() {
|
||||
rootCaCert = rootCa.certificate
|
||||
this.doormanCa = doormanCa
|
||||
networkMapCa = createDevNetworkMapCa(rootCa)
|
||||
val (caCrlPath, emptyCrlPath) = generateEmptyCrls(tempFolder, rootCa, getCaCrlEndpoint(serverAddress), getEmptyCrlEndpoint(serverAddress))
|
||||
revocationConfig = createCertificateRevocationConfig(emptyCrlPath, caCrlPath)
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -11,8 +11,7 @@
|
||||
package com.r3.corda.networkmanage.hsm
|
||||
|
||||
import com.nhaarman.mockito_kotlin.*
|
||||
import com.r3.corda.networkmanage.common.HOST
|
||||
import com.r3.corda.networkmanage.common.HsmBaseTest
|
||||
import com.r3.corda.networkmanage.common.*
|
||||
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
||||
import com.r3.corda.networkmanage.doorman.CertificateRevocationConfig
|
||||
import com.r3.corda.networkmanage.doorman.DoormanConfig
|
||||
@ -26,9 +25,6 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.hours
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.NodeRegistrationOption
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
@ -40,6 +36,7 @@ import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||
@ -48,6 +45,7 @@ import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import javax.persistence.PersistenceException
|
||||
@ -59,25 +57,31 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
private val portAllocation = PortAllocation.Incremental(10000)
|
||||
private val serverAddress = portAllocation.nextHostAndPort()
|
||||
|
||||
private lateinit var timer: Timer
|
||||
private lateinit var rootCaCert: X509Certificate
|
||||
private lateinit var intermediateCa: CertificateAndKeyPair
|
||||
private val timeoutMillis = 5.seconds.toMillis()
|
||||
|
||||
private lateinit var dbName: String
|
||||
|
||||
private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null)
|
||||
private val revocationConfig: CertificateRevocationConfig
|
||||
get() = CertificateRevocationConfig(
|
||||
private lateinit var revocationConfig: CertificateRevocationConfig
|
||||
|
||||
private fun createCertificateRevocationConfig(emptyCrlPath: Path, caCrlPath: Path): CertificateRevocationConfig {
|
||||
return CertificateRevocationConfig(
|
||||
approveAll = true,
|
||||
jira = null,
|
||||
crlCacheTimeout = 30.minutes.toMillis(),
|
||||
approveInterval = 10.minutes.toMillis(),
|
||||
approveInterval = timeoutMillis,
|
||||
crlCacheTimeout = timeoutMillis,
|
||||
localSigning = CertificateRevocationConfig.LocalSigning(
|
||||
crlEndpoint = URL("http://test.com/crl"),
|
||||
crlUpdateInterval = 2.hours.toMillis()
|
||||
)
|
||||
)
|
||||
|
||||
crlEndpoint = getNodeCrlEndpoint(serverAddress),
|
||||
crlUpdateInterval = timeoutMillis),
|
||||
emptyCrlPath = emptyCrlPath,
|
||||
caCrlPath = caCrlPath)
|
||||
}
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
@ -87,6 +91,8 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
|
||||
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||
rootCaCert = rootCa.certificate
|
||||
this.intermediateCa = intermediateCa
|
||||
val (caCrlPath, emptyCrlPath) = generateEmptyCrls(tempFolder, rootCa, getCaCrlEndpoint(serverAddress), getEmptyCrlEndpoint(serverAddress))
|
||||
revocationConfig = createCertificateRevocationConfig(emptyCrlPath, caCrlPath)
|
||||
}
|
||||
|
||||
@After
|
||||
@ -116,7 +122,7 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
|
||||
//Start doorman server
|
||||
NetworkManagementServer(makeTestDataSourceProperties(), makeTestDatabaseProperties(), doormanConfig, revocationConfig).use { server ->
|
||||
server.start(
|
||||
hostAndPort = NetworkHostAndPort(HOST, 0),
|
||||
hostAndPort = serverAddress,
|
||||
csrCertPathAndKey = null,
|
||||
startNetworkMap = null)
|
||||
val doormanHostAndPort = server.hostAndPort
|
||||
|
@ -26,7 +26,7 @@ class PersistentCertificateRevocationListStorage(private val database: CordaPers
|
||||
|
||||
override fun saveCertificateRevocationList(crl: X509CRL, crlIssuer: CrlIssuer, signedBy: String, revokedAt: Instant) {
|
||||
database.transaction {
|
||||
crl.revokedCertificates.forEach {
|
||||
crl.revokedCertificates?.forEach {
|
||||
revokeCertificate(it.serialNumber, revokedAt, this)
|
||||
}
|
||||
session.save(CertificateRevocationListEntity(
|
||||
|
@ -5,6 +5,7 @@ import com.r3.corda.networkmanage.common.persistence.entity.CertificateRevocatio
|
||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
||||
@ -25,6 +26,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
|
||||
CRLReason.SUPERSEDED,
|
||||
CRLReason.UNSPECIFIED
|
||||
)
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
override fun saveRevocationRequest(request: CertificateRevocationRequest): String {
|
||||
@ -60,7 +62,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
|
||||
}
|
||||
}
|
||||
|
||||
private fun validate(request:CertificateRevocationRequest) {
|
||||
private fun validate(request: CertificateRevocationRequest) {
|
||||
require(request.reason in ALLOWED_REASONS) { "The given revocation reason is not allowed." }
|
||||
}
|
||||
|
||||
@ -140,11 +142,20 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
|
||||
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
|
||||
))
|
||||
when (revocation.status) {
|
||||
RequestStatus.TICKET_CREATED -> {
|
||||
session.merge(revocation.copy(
|
||||
status = RequestStatus.APPROVED,
|
||||
modifiedAt = Instant.now(),
|
||||
modifiedBy = approvedBy
|
||||
))
|
||||
logger.debug("`request id` = $requestId marked as APPROVED")
|
||||
}
|
||||
else -> {
|
||||
logger.warn("`request id` = $requestId cannot be marked as APPROVED. Its current status is ${revocation.status}")
|
||||
return@transaction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -155,27 +166,45 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
|
||||
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
|
||||
))
|
||||
when (revocation.status) {
|
||||
RequestStatus.TICKET_CREATED -> {
|
||||
session.merge(revocation.copy(
|
||||
status = RequestStatus.REJECTED,
|
||||
modifiedAt = Instant.now(),
|
||||
modifiedBy = rejectedBy,
|
||||
remark = reason
|
||||
))
|
||||
logger.debug("`request id` = $requestId marked as REJECTED")
|
||||
}
|
||||
else -> {
|
||||
logger.warn("`request id` = $requestId cannot be marked as REJECTED. Its current status is ${revocation.status}")
|
||||
return@transaction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markRequestTicketCreated(requestId: String) {
|
||||
// Even though, we have an assumption that there is always a single instance of the doorman service running,
|
||||
// the SERIALIZABLE isolation level is used here just to ensure data consistency between the updates.
|
||||
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||
val request = requireNotNull(getRevocationRequestEntity(requestId, RequestStatus.NEW)) {
|
||||
"Error when creating request ticket with id: $requestId. Request does not exist or its status is not NEW."
|
||||
database.transaction {
|
||||
val revocation = getRevocationRequestEntity(requestId)
|
||||
if (revocation == null) {
|
||||
throw NoSuchElementException("Error while marking the request as ticket created! Certificate revocation id=$id does not exist")
|
||||
} else {
|
||||
when (revocation.status) {
|
||||
RequestStatus.NEW -> {
|
||||
session.merge(revocation.copy(
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.TICKET_CREATED
|
||||
))
|
||||
logger.debug("`request id` = $requestId marked as TICKED_CREATED")
|
||||
}
|
||||
else -> {
|
||||
logger.warn("`request id` = $requestId cannot be marked as TICKED_CREATED. Its current status is ${revocation.status}")
|
||||
return@transaction
|
||||
}
|
||||
}
|
||||
}
|
||||
val update = request.copy(
|
||||
modifiedAt = Instant.now(),
|
||||
status = RequestStatus.TICKET_CREATED)
|
||||
session.merge(update)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers
|
||||
existingRequestByPubKeyHash?.let {
|
||||
// Compare subject, attribute.
|
||||
// We cannot compare the request directly because it contains nonce.
|
||||
if (it.request.subject == request.subject && it.request.attributes.asList() == request.attributes.asList()) {
|
||||
if (certNotRevoked(it) && it.request.subject == request.subject && it.request.attributes.asList() == request.attributes.asList()) {
|
||||
return it.requestId
|
||||
} else {
|
||||
//TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
|
||||
@ -190,11 +190,19 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers
|
||||
// TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
|
||||
// Also, at the moment we assume that once the CSR is approved it cannot be rejected.
|
||||
// What if we approved something by mistake.
|
||||
if (nonRejectedRequest(CertificateSigningRequestEntity::legalName.name, legalName) != null) throw RequestValidationException(legalName, rejectMessage = "Duplicate legal name")
|
||||
|
||||
val existingRequestByLegalName = nonRejectedRequest(CertificateSigningRequestEntity::legalName.name, legalName)
|
||||
existingRequestByLegalName?.let {
|
||||
if (certNotRevoked(it)) {
|
||||
throw RequestValidationException(legalName, rejectMessage = "Duplicate legal name")
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun certNotRevoked(request: CertificateSigningRequestEntity): Boolean {
|
||||
return request.status != RequestStatus.DONE || request.certificateData?.certificateStatus != CertificateStatus.REVOKED
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve "non-rejected" request which matches provided column and value predicate.
|
||||
*/
|
||||
|
@ -23,14 +23,15 @@ fun createSignedCrl(issuerCertificate: X509Certificate,
|
||||
endpointUrl: URL,
|
||||
nextUpdateInterval: Duration,
|
||||
signer: Signer,
|
||||
includeInCrl: List<CertificateRevocationRequestData>): X509CRL {
|
||||
includeInCrl: List<CertificateRevocationRequestData>,
|
||||
indirectIssuingPoint: Boolean = false): X509CRL {
|
||||
val extensionUtils = JcaX509ExtensionUtils()
|
||||
val builder = X509v2CRLBuilder(X500Name.getInstance(issuerCertificate.issuerX500Principal.encoded), Date())
|
||||
val builder = X509v2CRLBuilder(X500Name.getInstance(issuerCertificate.subjectX500Principal.encoded), Date())
|
||||
builder.addExtension(Extension.authorityKeyIdentifier, false, extensionUtils.createAuthorityKeyIdentifier(issuerCertificate))
|
||||
val issuingDistributionPointName = GeneralName(GeneralName.uniformResourceIdentifier, endpointUrl.toString())
|
||||
val issuingDistributionPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistributionPointName)), false, false)
|
||||
val issuingDistributionPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistributionPointName)), indirectIssuingPoint, false)
|
||||
builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistributionPoint)
|
||||
builder.setNextUpdate(Date((Instant.now() + nextUpdateInterval).toEpochMilli()))
|
||||
builder.setNextUpdate(Date(Instant.now().toEpochMilli() + nextUpdateInterval.toMillis()))
|
||||
includeInCrl.forEach {
|
||||
builder.addCRLEntry(it.certificateSerialNumber, Date(it.modifiedAt.toEpochMilli()), it.reason.ordinal)
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import com.atlassian.jira.rest.client.api.JiraRestClient
|
||||
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
||||
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestData
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.contextLogger
|
||||
|
||||
class CrrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClient(restClient, projectCode) {
|
||||
@ -31,20 +32,29 @@ class CrrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClien
|
||||
"Certificate serial number: ${revocationRequest.certificateSerialNumber}\n" +
|
||||
"Revocation reason: ${revocationRequest.reason.name}\n" +
|
||||
"Reporter: ${revocationRequest.reporter}\n" +
|
||||
"CSR request ID: ${revocationRequest.certificateSigningRequestId}"
|
||||
"Original CSR request ID: ${revocationRequest.certificateSigningRequestId}"
|
||||
|
||||
val subject = CordaX500Name.build(revocationRequest.certificate.subjectX500Principal)
|
||||
val ticketSummary = if (subject.organisationUnit != null) {
|
||||
"${subject.organisationUnit}, ${subject.organisation}"
|
||||
} else {
|
||||
subject.organisation
|
||||
}
|
||||
|
||||
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
|
||||
.setProjectKey(projectCode)
|
||||
.setDescription(ticketDescription)
|
||||
.setSummary(ticketSummary)
|
||||
.setFieldValue(requestIdField.id, revocationRequest.requestId)
|
||||
// This will block until the issue is created.
|
||||
val issueId = restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim().key
|
||||
val createdIssue = checkNotNull(getIssueById(issueId)) { "Missing the JIRA ticket for the request ID: $issueId" }
|
||||
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim().key
|
||||
val createdIssue = checkNotNull(getIssueById(revocationRequest.requestId)) { "Missing the JIRA ticket for the request ID: ${revocationRequest.requestId}" }
|
||||
restClient.issueClient.addAttachment(createdIssue.attachmentsUri, revocationRequest.certificate.encoded.inputStream(), "${revocationRequest.certificateSerialNumber}.cer")
|
||||
.fail { CsrJiraClient.logger.error("Error processing request '${createdIssue.key}' : Exception when uploading attachment to JIRA.", it) }.claim()
|
||||
.fail { logger.error("Error processing request '${createdIssue.key}' : Exception when uploading attachment to JIRA.", it) }.claim()
|
||||
}
|
||||
|
||||
fun updateDoneCertificateRevocationRequest(requestId: String) {
|
||||
logger.debug("Marking JIRA ticket with `request ID` = $requestId as DONE.")
|
||||
val issue = requireNotNull(getIssueById(requestId)) { "Missing the JIRA ticket for the request ID: $requestId" }
|
||||
restClient.issueClient.transition(issue, TransitionInput(getTransitionId(DONE_TRANSITION_KEY, issue))).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.google.common.primitives.Booleans
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.config.OldConfig
|
||||
@ -41,7 +42,15 @@ data class NetworkManagementServerConfig( // TODO: Move local signing to signing
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val rootKeystorePassword: String?,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val rootPrivateKeyPassword: String?
|
||||
val rootPrivateKeyPassword: String?,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val caCrlPath: Path? = null,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val caCrlUrl: URL? = null,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val emptyCrlPath: Path? = null,
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
val emptyCrlUrl: URL? = null
|
||||
) {
|
||||
companion object {
|
||||
// TODO: Do we really need these defaults?
|
||||
@ -53,6 +62,7 @@ data class NetworkManagementServerConfig( // TODO: Move local signing to signing
|
||||
data class DoormanConfig(val approveAll: Boolean = false,
|
||||
@OldConfig("jiraConfig")
|
||||
val jira: JiraConfig? = null,
|
||||
val crlEndpoint: URL? = null,
|
||||
val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis()) {
|
||||
init {
|
||||
require(Booleans.countTrue(approveAll, jira != null) == 1) {
|
||||
@ -65,6 +75,8 @@ data class CertificateRevocationConfig(val approveAll: Boolean = false,
|
||||
val jira: JiraConfig? = null,
|
||||
val localSigning: LocalSigning?,
|
||||
val crlCacheTimeout: Long,
|
||||
val caCrlPath: Path,
|
||||
val emptyCrlPath: Path,
|
||||
val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis()) {
|
||||
init {
|
||||
require(Booleans.countTrue(approveAll, jira != null) == 1) {
|
||||
|
@ -15,6 +15,7 @@ import com.r3.corda.networkmanage.common.utils.*
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -36,7 +37,14 @@ fun main(args: Array<String>) {
|
||||
|
||||
logger.info("Running in ${cmdLineOptions.mode} mode")
|
||||
when (cmdLineOptions.mode) {
|
||||
Mode.ROOT_KEYGEN -> rootKeyGenMode(cmdLineOptions, config)
|
||||
Mode.ROOT_KEYGEN -> {
|
||||
val emptyCrlPath = requireNotNull(config.emptyCrlPath) { "emptyCrlPath needs to be specified" }
|
||||
val emptyCrlUrl = requireNotNull(config.emptyCrlUrl) { "emptyCrlUrl needs to be specified" }
|
||||
val caCrlPath = requireNotNull(config.caCrlPath) { "caCrlPath needs to be specified" }
|
||||
val caCrlUrl = requireNotNull(config.caCrlUrl) { "caCrlUrl needs to be specified" }
|
||||
val rootCertificateAndKeyPair = rootKeyGenMode(cmdLineOptions, config)
|
||||
createEmptyCrls(rootCertificateAndKeyPair, emptyCrlPath, emptyCrlUrl, caCrlPath, caCrlUrl)
|
||||
}
|
||||
Mode.CA_KEYGEN -> caKeyGenMode(config)
|
||||
Mode.DOORMAN -> doormanMode(cmdLineOptions, config)
|
||||
}
|
||||
@ -61,8 +69,8 @@ private fun processKeyStore(config: NetworkManagementServerConfig): Pair<CertPat
|
||||
return Pair(csrCertPathAndKey, networkMapSigner)
|
||||
}
|
||||
|
||||
private fun rootKeyGenMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig) {
|
||||
generateRootKeyPair(
|
||||
private fun rootKeyGenMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig): CertificateAndKeyPair {
|
||||
return generateRootKeyPair(
|
||||
requireNotNull(config.rootStorePath) { "The 'rootStorePath' parameter must be specified when generating keys!" },
|
||||
config.rootKeystorePassword,
|
||||
config.rootPrivateKeyPassword,
|
||||
@ -77,7 +85,8 @@ private fun caKeyGenMode(config: NetworkManagementServerConfig) {
|
||||
config.rootKeystorePassword,
|
||||
config.rootPrivateKeyPassword,
|
||||
config.keystorePassword,
|
||||
config.caPrivateKeyPassword
|
||||
config.caPrivateKeyPassword,
|
||||
config.caCrlUrl
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.Closeable
|
||||
import java.net.URI
|
||||
import java.time.Duration
|
||||
@ -94,9 +95,9 @@ class NetworkManagementServer(dataSourceProperties: Properties,
|
||||
val requestProcessor = if (jiraConfig != null) {
|
||||
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
|
||||
val jiraClient = CsrJiraClient(jiraWebAPI, jiraConfig.projectCode)
|
||||
JiraCsrHandler(jiraClient, csrStorage, DefaultCsrHandler(csrStorage, csrCertPathAndKey))
|
||||
JiraCsrHandler(jiraClient, csrStorage, DefaultCsrHandler(csrStorage, csrCertPathAndKey, config.crlEndpoint))
|
||||
} else {
|
||||
DefaultCsrHandler(csrStorage, csrCertPathAndKey)
|
||||
DefaultCsrHandler(csrStorage, csrCertPathAndKey, config.crlEndpoint)
|
||||
}
|
||||
|
||||
val scheduledExecutor = Executors.newScheduledThreadPool(1)
|
||||
@ -131,7 +132,7 @@ class NetworkManagementServer(dataSourceProperties: Properties,
|
||||
val crlHandler = csrCertPathAndKeyPair?.let {
|
||||
LocalCrlHandler(crrStorage,
|
||||
crlStorage,
|
||||
CertificateAndKeyPair(it.certPath.first(), it.toKeyPair()),
|
||||
CertificateAndKeyPair(it.certPath[0], it.toKeyPair()),
|
||||
Duration.ofMillis(config.localSigning!!.crlUpdateInterval),
|
||||
config.localSigning.crlEndpoint)
|
||||
}
|
||||
@ -158,7 +159,13 @@ class NetworkManagementServer(dataSourceProperties: Properties,
|
||||
scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
|
||||
closeActions += scheduledExecutor::shutdown
|
||||
// TODO start socket server
|
||||
return Pair(CertificateRevocationRequestWebService(crrHandler), CertificateRevocationListWebService(crlStorage, Duration.ofMillis(config.crlCacheTimeout)))
|
||||
return Pair(
|
||||
CertificateRevocationRequestWebService(crrHandler),
|
||||
CertificateRevocationListWebService(
|
||||
crlStorage,
|
||||
FileUtils.readFileToByteArray(config.caCrlPath.toFile()),
|
||||
FileUtils.readFileToByteArray(config.emptyCrlPath.toFile()),
|
||||
Duration.ofMillis(config.crlCacheTimeout)))
|
||||
}
|
||||
|
||||
fun start(hostAndPort: NetworkHostAndPort,
|
||||
|
@ -11,10 +11,14 @@
|
||||
package com.r3.corda.networkmanage.doorman
|
||||
|
||||
import com.r3.corda.networkmanage.common.utils.CORDA_NETWORK_MAP
|
||||
import com.r3.corda.networkmanage.common.utils.createSignedCrl
|
||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
|
||||
@ -22,6 +26,8 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.createCertificate
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.createSelfSignedCACertificate
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.system.exitProcess
|
||||
@ -41,7 +47,7 @@ internal fun readPassword(fmt: String): String {
|
||||
}
|
||||
|
||||
// Keygen utilities.
|
||||
fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, networkRootTrustPass: String?) {
|
||||
fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, networkRootTrustPass: String?): CertificateAndKeyPair {
|
||||
println("Generating Root CA keypair and certificate.")
|
||||
// Get password from console if not in config.
|
||||
val rootKeystorePassword = rootKeystorePass ?: readPassword("Root Keystore Password: ")
|
||||
@ -76,9 +82,22 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv
|
||||
println("Trust store for distribution to nodes created in $trustStorePath")
|
||||
println("Root CA keypair and certificate stored in ${rootStoreFile.toAbsolutePath()}.")
|
||||
println(rootCert)
|
||||
return CertificateAndKeyPair(rootCert, selfSignKey)
|
||||
}
|
||||
|
||||
fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) {
|
||||
fun createEmptyCrls(rootCertificateAndKeyPair: CertificateAndKeyPair, emptyCrlPath: Path, emptyCrlUrl: URL, caCrlPath: Path, caCrlUrl: URL) {
|
||||
val rootCert = rootCertificateAndKeyPair.certificate
|
||||
val rootKey = rootCertificateAndKeyPair.keyPair.private
|
||||
val emptyCrl = createSignedCrl(rootCert, emptyCrlUrl, 3650.days, LocalSigner(rootKey, rootCert), emptyList(), true)
|
||||
FileUtils.writeByteArrayToFile(emptyCrlPath.toFile(), emptyCrl.encoded)
|
||||
val caCrl = createSignedCrl(rootCert, caCrlUrl, 3650.days, LocalSigner(rootKey, rootCert), emptyList())
|
||||
FileUtils.writeByteArrayToFile(caCrlPath.toFile(), caCrl.encoded)
|
||||
println("Empty CRL: $emptyCrl")
|
||||
println("CA CRL: $caCrl")
|
||||
println("Root signed empty and CA CRL files created in $emptyCrlPath and $caCrlPath respectively")
|
||||
}
|
||||
|
||||
fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?, caCrlUrl: URL?) {
|
||||
println("Generating intermediate and network map key pairs and certificates using root key store $rootStoreFile.")
|
||||
// Get password from console if not in config.
|
||||
val rootKeystorePassword = rootKeystorePass ?: readPassword("Root key store password: ")
|
||||
@ -106,7 +125,8 @@ fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystor
|
||||
rootKeyPairAndCert.certificate,
|
||||
rootKeyPairAndCert.keyPair,
|
||||
subject,
|
||||
keyPair.public
|
||||
keyPair.public,
|
||||
crlDistPoint = caCrlUrl?.toString()
|
||||
)
|
||||
|
||||
keyStore.update {
|
||||
|
@ -11,18 +11,18 @@
|
||||
package com.r3.corda.networkmanage.doorman.signer
|
||||
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage.Companion.DOORMAN_SIGNATURE
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage
|
||||
import com.r3.corda.networkmanage.common.persistence.CrlIssuer
|
||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||
import com.r3.corda.networkmanage.common.signer.CertificateRevocationListSigner
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
import java.net.URL
|
||||
import java.time.Duration
|
||||
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage.Companion.DOORMAN_SIGNATURE
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
|
||||
class LocalCrlHandler(private val crrStorage: CertificateRevocationRequestStorage,
|
||||
crlStorage: CertificateRevocationListStorage,
|
||||
private val crlStorage: CertificateRevocationListStorage,
|
||||
issuerCertAndKey: CertificateAndKeyPair,
|
||||
crlUpdateInterval: Duration,
|
||||
crlEndpoint: URL) {
|
||||
@ -38,19 +38,22 @@ class LocalCrlHandler(private val crrStorage: CertificateRevocationRequestStorag
|
||||
LocalSigner(issuerCertAndKey))
|
||||
|
||||
fun signCrl() {
|
||||
if (crlStorage.getCertificateRevocationList(CrlIssuer.DOORMAN) == null) {
|
||||
val crl = crlSigner.createSignedCRL(emptyList(), emptyList(), DOORMAN_SIGNATURE)
|
||||
logger.info("Saving a new empty CRL: $crl")
|
||||
return
|
||||
}
|
||||
logger.info("Executing CRL signing...")
|
||||
val approvedRequests = crrStorage.getRevocationRequests(RequestStatus.APPROVED)
|
||||
logger.debug("Approved certificate revocation requests retrieved.")
|
||||
logger.trace { approvedRequests.toString() }
|
||||
logger.debug("Approved certificate revocation requests retrieved: $approvedRequests")
|
||||
if (approvedRequests.isEmpty()) {
|
||||
// Nothing to add to the current CRL
|
||||
logger.debug("There are no APPROVED certificate revocation requests. Aborting CRL signing.")
|
||||
return
|
||||
}
|
||||
val currentRequests = crrStorage.getRevocationRequests(RequestStatus.DONE)
|
||||
logger.debug("Existing certificate revocation requests retrieved.")
|
||||
logger.trace { currentRequests.toString() }
|
||||
crlSigner.createSignedCRL(approvedRequests, currentRequests, DOORMAN_SIGNATURE)
|
||||
logger.info("New CRL signed.")
|
||||
logger.debug("Existing certificate revocation requests retrieved: $currentRequests")
|
||||
val crl = crlSigner.createSignedCRL(approvedRequests, currentRequests, DOORMAN_SIGNATURE)
|
||||
logger.info("New CRL signed: $crl")
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||
import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||
import java.net.URL
|
||||
import java.security.cert.CertPath
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
@ -34,7 +35,8 @@ interface CsrHandler {
|
||||
}
|
||||
|
||||
class DefaultCsrHandler(private val storage: CertificateSigningRequestStorage,
|
||||
private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler {
|
||||
private val csrCertPathAndKey: CertPathAndKey?,
|
||||
private val crlDistributionPoint: URL? = null) : CsrHandler {
|
||||
|
||||
override fun processRequests() {
|
||||
if (csrCertPathAndKey == null) return
|
||||
@ -75,7 +77,8 @@ class DefaultCsrHandler(private val storage: CertificateSigningRequestStorage,
|
||||
csrCertPathAndKey.toKeyPair(),
|
||||
X500Principal(request.subject.encoded),
|
||||
request.publicKey,
|
||||
nameConstraints = nameConstraints)
|
||||
nameConstraints = nameConstraints,
|
||||
crlDistPoint = crlDistributionPoint?.toString())
|
||||
return X509CertificateFactory().generateCertPath(listOf(nodeCaCert) + csrCertPathAndKey.certPath)
|
||||
}
|
||||
}
|
||||
|
@ -59,10 +59,10 @@ class JiraCrrHandler(private val jiraClient: CrrJiraClient,
|
||||
|
||||
private fun updateJiraTickets(approvedRequest: List<ApprovedRequest>, rejectedRequest: List<RejectedRequest>) {
|
||||
// Reconfirm request status and update jira status
|
||||
logger.debug("Updating JIRA tickets: `approved` = $approvedRequest, `rejected` = $rejectedRequest")
|
||||
approvedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) }
|
||||
.filter { it.status == RequestStatus.DONE }
|
||||
.forEachWithExceptionLogging(logger) { jiraClient.updateDoneCertificateRevocationRequest(it.requestId) }
|
||||
|
||||
rejectedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) }
|
||||
.filter { it.status == RequestStatus.REJECTED }
|
||||
.forEachWithExceptionLogging(logger) { jiraClient.updateRejectedRequest(it.requestId) }
|
||||
|
@ -17,6 +17,8 @@ import javax.ws.rs.core.Response.status
|
||||
|
||||
@Path(CRL_PATH)
|
||||
class CertificateRevocationListWebService(private val revocationListStorage: CertificateRevocationListStorage,
|
||||
private val caCrlBytes: ByteArray,
|
||||
private val emptyCrlBytes: ByteArray,
|
||||
cacheTimeout: Duration) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
@ -24,6 +26,7 @@ class CertificateRevocationListWebService(private val revocationListStorage: Cer
|
||||
const val CRL_DATA_TYPE = "application/pkcs7-crl"
|
||||
const val DOORMAN = "doorman"
|
||||
const val ROOT = "root"
|
||||
const val EMPTY = "empty"
|
||||
}
|
||||
|
||||
private val crlCache: LoadingCache<CrlIssuer, ByteArray> = Caffeine.newBuilder()
|
||||
@ -43,7 +46,14 @@ class CertificateRevocationListWebService(private val revocationListStorage: Cer
|
||||
@Path(ROOT)
|
||||
@Produces(CRL_DATA_TYPE)
|
||||
fun getRootRevocationList(): Response {
|
||||
return getCrlResponse(CrlIssuer.ROOT)
|
||||
return ok(caCrlBytes).build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(EMPTY)
|
||||
@Produces(CRL_DATA_TYPE)
|
||||
fun getEmptyRevocationList(): Response {
|
||||
return ok(emptyCrlBytes).build()
|
||||
}
|
||||
|
||||
private fun getCrlResponse(issuer: CrlIssuer): Response {
|
||||
|
@ -0,0 +1,108 @@
|
||||
package com.r3.corda.networkmanage
|
||||
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
import net.corda.testing.core.freePort
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.Security
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* This test is to perform manual testing of the SSL connection using local key stores. It aims to assess the
|
||||
* correct behaviour of the SSL connection between 2 nodes with respect to the CRL validation.
|
||||
* In order to debug the certificate path validation please use the following JVM parameters when running the test:
|
||||
* -Djavax.net.debug=ssl,handshake -Djava.security.debug=certpath
|
||||
*/
|
||||
@Ignore
|
||||
class CertificateRevocationListNodeTests {
|
||||
@Rule
|
||||
@JvmField
|
||||
val temporaryFolder = TemporaryFolder()
|
||||
|
||||
private val serverPort = freePort()
|
||||
|
||||
private val serverSslKeyStore: Path = Paths.get("/certificatesServer/sslkeystore.jks")
|
||||
private val clientSslKeyStore: Path = Paths.get("/certificatesClient/sslkeystore.jks")
|
||||
private val serverTrustStore: Path = Paths.get("/certificatesServer/truststore.jks")
|
||||
private val clientTrustStore: Path = Paths.get("/certificatesClient/truststore.jks")
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Simple AMPQ Client to Server connection works`() {
|
||||
val amqpServer = createServer(serverPort)
|
||||
amqpServer.use {
|
||||
amqpServer.start()
|
||||
val receiveSubs = amqpServer.onReceive.subscribe {
|
||||
assertEquals(BOB_NAME.toString(), it.sourceLegalName)
|
||||
assertEquals(P2P_PREFIX + "Test", it.topic)
|
||||
assertEquals("Test", String(it.payload))
|
||||
it.complete(true)
|
||||
}
|
||||
val amqpClient = createClient(serverPort)
|
||||
amqpClient.use {
|
||||
val serverConnected = amqpServer.onConnection.toFuture()
|
||||
val clientConnected = amqpClient.onConnection.toFuture()
|
||||
amqpClient.start()
|
||||
val serverConnect = serverConnected.get()
|
||||
assertEquals(true, serverConnect.connected)
|
||||
val clientConnect = clientConnected.get()
|
||||
assertEquals(true, clientConnect.connected)
|
||||
val msg = amqpClient.createMessage("Test".toByteArray(),
|
||||
P2P_PREFIX + "Test",
|
||||
ALICE_NAME.toString(),
|
||||
emptyMap())
|
||||
amqpClient.write(msg)
|
||||
assertEquals(MessageStatus.Acknowledged, msg.onComplete.get())
|
||||
receiveSubs.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createClient(targetPort: Int): AMQPClient {
|
||||
val tS = X509KeyStore.fromFile(clientTrustStore, "trustpass").internal
|
||||
val sslS = X509KeyStore.fromFile(clientSslKeyStore, "cordacadevpass").internal
|
||||
return AMQPClient(
|
||||
listOf(NetworkHostAndPort("localhost", targetPort)),
|
||||
setOf(ALICE_NAME, CHARLIE_NAME),
|
||||
PEER_USER,
|
||||
PEER_USER,
|
||||
sslS,
|
||||
"cordacadevpass",
|
||||
tS,
|
||||
false)
|
||||
}
|
||||
|
||||
private fun createServer(port: Int): AMQPServer {
|
||||
val tS = X509KeyStore.fromFile(serverTrustStore, "trustpass").internal
|
||||
val sslS = X509KeyStore.fromFile(serverSslKeyStore, "cordacadevpass").internal
|
||||
return AMQPServer(
|
||||
"0.0.0.0",
|
||||
port,
|
||||
PEER_USER,
|
||||
PEER_USER,
|
||||
sslS,
|
||||
"cordacadevpass",
|
||||
tS,
|
||||
false)
|
||||
}
|
||||
}
|
@ -57,6 +57,7 @@ class PersistentCertificateRevocationListStorageTest : TestBase() {
|
||||
certificateSerialNumber = certificate.serialNumber,
|
||||
reason = REVOCATION_REASON,
|
||||
reporter = REPORTER))
|
||||
crrStorage.markRequestTicketCreated(requestId)
|
||||
crrStorage.approveRevocationRequest(requestId, "Approver")
|
||||
val revocationRequest = crrStorage.getRevocationRequest(requestId)!!
|
||||
val crl = createDummyCertificateRevocationList(listOf(revocationRequest.certificateSerialNumber))
|
||||
@ -80,6 +81,7 @@ class PersistentCertificateRevocationListStorageTest : TestBase() {
|
||||
certificateSerialNumber = createNodeCertificate(csrStorage, legalName = "Bank A").serialNumber,
|
||||
reason = REVOCATION_REASON,
|
||||
reporter = REPORTER))
|
||||
crrStorage.markRequestTicketCreated(done)
|
||||
crrStorage.approveRevocationRequest(done, "Approver")
|
||||
val doneRevocationRequest = crrStorage.getRevocationRequest(done)!!
|
||||
|
||||
@ -95,6 +97,7 @@ class PersistentCertificateRevocationListStorageTest : TestBase() {
|
||||
certificateSerialNumber = createNodeCertificate(csrStorage, legalName = "Bank C").serialNumber,
|
||||
reason = REVOCATION_REASON,
|
||||
reporter = REPORTER))
|
||||
crrStorage.markRequestTicketCreated(approved)
|
||||
crrStorage.approveRevocationRequest(approved, "Approver")
|
||||
val approvedRevocationRequest = crrStorage.getRevocationRequest(approved)!!
|
||||
|
||||
|
@ -85,6 +85,7 @@ class PersistentCertificateRevocationRequestStorageTest : TestBase() {
|
||||
certificateSerialNumber = createNodeCertificate(csrStorage, "LegalName" + it.toString()).serialNumber,
|
||||
reason = REVOCATION_REASON,
|
||||
reporter = REPORTER))
|
||||
crrStorage.markRequestTicketCreated(requestId)
|
||||
crrStorage.approveRevocationRequest(requestId, "Approver")
|
||||
}
|
||||
|
||||
@ -117,6 +118,7 @@ class PersistentCertificateRevocationRequestStorageTest : TestBase() {
|
||||
certificateSerialNumber = certificate.serialNumber,
|
||||
reason = REVOCATION_REASON,
|
||||
reporter = REPORTER))
|
||||
crrStorage.markRequestTicketCreated(requestId)
|
||||
|
||||
// when
|
||||
crrStorage.approveRevocationRequest(requestId, "Approver")
|
||||
@ -135,6 +137,7 @@ class PersistentCertificateRevocationRequestStorageTest : TestBase() {
|
||||
certificateSerialNumber = certificate.serialNumber,
|
||||
reason = REVOCATION_REASON,
|
||||
reporter = REPORTER))
|
||||
crrStorage.markRequestTicketCreated(requestId)
|
||||
|
||||
// when
|
||||
crrStorage.rejectRevocationRequest(requestId, "Rejector", "No reason")
|
||||
|
@ -28,6 +28,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigT
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.tools.shell.SSHDConfiguration
|
||||
import org.slf4j.Logger
|
||||
import sun.security.x509.X500Name
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
@ -70,6 +71,8 @@ interface NodeConfiguration : NodeSSLConfiguration {
|
||||
// do not change this value without syncing it with ScheduledFlowsDrainingModeTest
|
||||
val drainingModePollPeriod: Duration get() = Duration.ofSeconds(5)
|
||||
val extraNetworkMapKeys: List<UUID>
|
||||
val tlsCertCrlDistPoint: URL?
|
||||
val tlsCertCrlIssuer: String?
|
||||
|
||||
fun validate(): List<String>
|
||||
|
||||
@ -190,6 +193,8 @@ data class NodeConfigurationImpl(
|
||||
override val crlCheckSoftFail: Boolean,
|
||||
override val dataSourceProperties: Properties,
|
||||
override val compatibilityZoneURL: URL? = null,
|
||||
override val tlsCertCrlDistPoint: URL? = null,
|
||||
override val tlsCertCrlIssuer: String? = null,
|
||||
override val rpcUsers: List<User>,
|
||||
override val security: SecurityConfiguration? = null,
|
||||
override val verifierType: VerifierType,
|
||||
@ -241,10 +246,29 @@ data class NodeConfigurationImpl(
|
||||
}.asOptions(fallbackSslOptions)
|
||||
}
|
||||
|
||||
private fun validateTlsCertCrlConfig(): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
if (tlsCertCrlIssuer != null) {
|
||||
if (tlsCertCrlDistPoint == null) {
|
||||
errors += "tlsCertCrlDistPoint needs to be specified when tlsCertCrlIssuer is not NULL"
|
||||
}
|
||||
try {
|
||||
X500Name(tlsCertCrlIssuer)
|
||||
} catch (e: Exception) {
|
||||
errors += "Error when parsing tlsCertCrlIssuer: ${e.message}"
|
||||
}
|
||||
}
|
||||
if (!crlCheckSoftFail && tlsCertCrlDistPoint == null) {
|
||||
errors += "tlsCertCrlDistPoint needs to be specified when crlCheckSoftFail is FALSE"
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
override fun validate(): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
errors += validateDevModeOptions()
|
||||
errors += validateRpcOptions(rpcOptions)
|
||||
errors += validateTlsCertCrlConfig()
|
||||
return errors
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ package net.corda.node.utilities.registration
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.NodeRegistrationOption
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
@ -22,6 +23,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||
import org.bouncycastle.util.io.pem.PemObject
|
||||
import java.io.StringWriter
|
||||
@ -226,6 +228,10 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService:
|
||||
CORDA_CLIENT_CA,
|
||||
CertRole.NODE_CA) {
|
||||
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
override fun onSuccess(nodeCAKeyPair: KeyPair, certificates: List<X509Certificate>) {
|
||||
createSSLKeystore(nodeCAKeyPair, certificates)
|
||||
createTruststore(certificates.last())
|
||||
@ -240,7 +246,10 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService:
|
||||
certificates.first(),
|
||||
nodeCAKeyPair,
|
||||
config.myLegalName.x500Principal,
|
||||
sslKeyPair.public)
|
||||
sslKeyPair.public,
|
||||
crlDistPoint = config.tlsCertCrlDistPoint?.toString(),
|
||||
crlIssuer = if (config.tlsCertCrlIssuer != null) X500Name(config.tlsCertCrlIssuer) else null)
|
||||
logger.info("Generated TLS certificate: $sslCert")
|
||||
setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates)
|
||||
}
|
||||
println("SSL private key and certificate stored in ${config.sslKeystore}.")
|
||||
|
@ -24,6 +24,7 @@ import net.corda.tools.shell.SSHDConfiguration
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.net.URL
|
||||
import java.net.URI
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
@ -42,6 +43,27 @@ class NodeConfigurationImplTest {
|
||||
configDebugOptions(false, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can't have tlsCertCrlDistPoint null when tlsCertCrlIssuer is given`() {
|
||||
val configValidationResult = configTlsCertCrlOptions(null, "C=US, L=New York, OU=Corda, O=R3 HoldCo LLC, CN=Corda Root CA").validate()
|
||||
assertTrue { configValidationResult.isNotEmpty() }
|
||||
assertThat(configValidationResult.first()).contains("tlsCertCrlDistPoint needs to be specified when tlsCertCrlIssuer is not NULL")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tlsCertCrlIssuer validation fails when misconfigured`() {
|
||||
val configValidationResult = configTlsCertCrlOptions(URL("http://test.com/crl"), "Corda Root CA").validate()
|
||||
assertTrue { configValidationResult.isNotEmpty() }
|
||||
assertThat(configValidationResult.first()).contains("Error when parsing tlsCertCrlIssuer:")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can't have tlsCertCrlDistPoint null when crlCheckSoftFail is false`() {
|
||||
val configValidationResult = configTlsCertCrlOptions(null, null, false).validate()
|
||||
assertTrue { configValidationResult.isNotEmpty() }
|
||||
assertThat(configValidationResult.first()).contains("tlsCertCrlDistPoint needs to be specified when crlCheckSoftFail is FALSE")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check devModeOptions flag helper`() {
|
||||
assertTrue { configDebugOptions(true, null).shouldCheckCheckpoints() }
|
||||
@ -155,6 +177,10 @@ class NodeConfigurationImplTest {
|
||||
return testConfiguration.copy(devMode = devMode, devModeOptions = devModeOptions)
|
||||
}
|
||||
|
||||
private fun configTlsCertCrlOptions(tlsCertCrlDistPoint: URL?, tlsCertCrlIssuer: String?, crlCheckSoftFail: Boolean = true): NodeConfiguration {
|
||||
return testConfiguration.copy(tlsCertCrlDistPoint = tlsCertCrlDistPoint, tlsCertCrlIssuer = tlsCertCrlIssuer, crlCheckSoftFail = crlCheckSoftFail)
|
||||
}
|
||||
|
||||
private fun testConfiguration(dataSourceProperties: Properties): NodeConfigurationImpl {
|
||||
return testConfiguration.copy(dataSourceProperties = dataSourceProperties)
|
||||
}
|
||||
@ -190,7 +216,8 @@ class NodeConfigurationImplTest {
|
||||
rpcSettings = rpcSettings,
|
||||
relay = null,
|
||||
enterpriseConfiguration = EnterpriseConfiguration((MutualExclusionConfiguration(false, "", 20000, 40000))),
|
||||
crlCheckSoftFail = true
|
||||
crlCheckSoftFail = true,
|
||||
tlsCertCrlDistPoint = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +68,8 @@ class NetworkRegistrationHelperTest {
|
||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
||||
doReturn(nodeLegalName).whenever(it).myLegalName
|
||||
doReturn("").whenever(it).emailAddress
|
||||
doReturn(null).whenever(it).tlsCertCrlDistPoint
|
||||
doReturn(null).whenever(it).tlsCertCrlIssuer
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,6 @@
|
||||
|
||||
package net.corda.testing.internal
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doAnswer
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.Crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
|
Loading…
x
Reference in New Issue
Block a user