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:
Michal Kit 2018-05-18 11:17:43 +01:00 committed by GitHub
parent 1c575b5364
commit da6957e6d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 446 additions and 87 deletions

View File

@ -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
--------

View File

@ -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

View File

@ -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)

View File

@ -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", "")) {

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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)
}
}

View File

@ -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.
*/

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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) {

View File

@ -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
)
}

View File

@ -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,

View File

@ -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 {

View File

@ -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")
}
}

View File

@ -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)
}
}

View File

@ -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) }

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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)!!

View File

@ -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")

View File

@ -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
}

View File

@ -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}.")

View File

@ -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
)
}
}

View File

@ -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
}
}

View File

@ -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