diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index a9151e6fe1..51f660a836 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -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 -------- diff --git a/docs/source/running-doorman.rst b/docs/source/running-doorman.rst index 451fb1f033..36b2dec670 100644 --- a/docs/source/running-doorman.rst +++ b/docs/source/running-doorman.rst @@ -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 diff --git a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt index 76ed6803b1..2ff23b6295 100644 --- a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt +++ b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt @@ -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) diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt index 369c62f705..b8dc16810e 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt @@ -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 { + 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", "")) { diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt index 4f7a215300..955d25871f 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt @@ -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 diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt index e7db87b120..db7d13bdb4 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt @@ -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 diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt index 16a5ee5ab3..0782d73754 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt @@ -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( diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt index aef8f8d8e9..42ec4db79b 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt @@ -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) } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt index c8b301bd05..e15cd61822 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt @@ -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. */ diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt index 1691a52e1b..bf48596fc9 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt @@ -23,14 +23,15 @@ fun createSignedCrl(issuerCertificate: X509Certificate, endpointUrl: URL, nextUpdateInterval: Duration, signer: Signer, - includeInCrl: List): X509CRL { + includeInCrl: List, + 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) } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt index f6d82df265..878696f4ff 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt @@ -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() } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt index 72e78be49e..088d9d14dc 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt @@ -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) { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt index 72da06ac1f..fecdcb19f5 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt @@ -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) { 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, rejectedRequest: List) { // 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) } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt index f5b98da759..1f2f4931c7 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt @@ -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 = 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 { diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/CertificateRevocationListNodeTests.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/CertificateRevocationListNodeTests.kt new file mode 100644 index 0000000000..be614852ca --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/CertificateRevocationListNodeTests.kt @@ -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) + } +} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt index f0200491c9..7b8bf81b87 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt @@ -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)!! diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt index bdeb2d604f..024799930e 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt @@ -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") diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index ad9d904de8..a6310e578a 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -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 + val tlsCertCrlDistPoint: URL? + val tlsCertCrlIssuer: String? fun validate(): List @@ -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, override val security: SecurityConfiguration? = null, override val verifierType: VerifierType, @@ -241,10 +246,29 @@ data class NodeConfigurationImpl( }.asOptions(fallbackSslOptions) } + private fun validateTlsCertCrlConfig(): List { + val errors = mutableListOf() + 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 { val errors = mutableListOf() errors += validateDevModeOptions() errors += validateRpcOptions(rpcOptions) + errors += validateTlsCertCrlConfig() return errors } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index a18316616f..7394c92eed 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -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) { 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}.") diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index 320591dcbf..2685f9f463 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -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 ) } } diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index 1e70b77eb8..a08bae3f05 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -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 } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 4c140b744e..87baa6449f 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -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