mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
ENT-4912: Enable CRL checking with embedded Artemis (#6154)
This commit is contained in:
parent
ad020647ab
commit
0d441c3760
@ -1594,7 +1594,7 @@
|
||||
<ID>TooGenericExceptionCaught:ReconnectingCordaRPCOps.kt$ReconnectingCordaRPCOps.ReconnectingRPCConnection$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:ReconnectingObservable.kt$ReconnectingObservable.ReconnectingSubscriber$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:RpcServerObservableSerializerTests.kt$RpcServerObservableSerializerTests$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SSLHelper.kt$LoggingTrustManagerWrapper$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SSLHelper.kt$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:ScheduledFlowIntegrationTests.kt$ScheduledFlowIntegrationTests$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SerializationOutputTests.kt$SerializationOutputTests$t: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:ShutdownManager.kt$ShutdownManager$t: Throwable</ID>
|
||||
|
@ -84,35 +84,35 @@ fun X509Certificate.distributionPointsToString() : String {
|
||||
}
|
||||
}
|
||||
|
||||
fun certPathToString(certPath: Array<out X509Certificate>?): String {
|
||||
if (certPath == null) {
|
||||
return "<empty certpath>"
|
||||
}
|
||||
val certs = certPath.map {
|
||||
val bcCert = it.toBc()
|
||||
val subject = bcCert.subject.toString()
|
||||
val issuer = bcCert.issuer.toString()
|
||||
val keyIdentifier = try {
|
||||
SubjectKeyIdentifier.getInstance(bcCert.getExtension(Extension.subjectKeyIdentifier).parsedValue).keyIdentifier.toHex()
|
||||
} catch (ex: Exception) {
|
||||
"null"
|
||||
}
|
||||
val authorityKeyIdentifier = try {
|
||||
AuthorityKeyIdentifier.getInstance(bcCert.getExtension(Extension.authorityKeyIdentifier).parsedValue).keyIdentifier.toHex()
|
||||
} catch (ex: Exception) {
|
||||
"null"
|
||||
}
|
||||
" $subject[$keyIdentifier] issued by $issuer[$authorityKeyIdentifier] [${it.distributionPointsToString()}]"
|
||||
}
|
||||
return certs.joinToString("\r\n")
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509ExtendedTrustManager() {
|
||||
companion object {
|
||||
val log = contextLogger()
|
||||
}
|
||||
|
||||
private fun certPathToString(certPath: Array<out X509Certificate>?): String {
|
||||
if (certPath == null) {
|
||||
return "<empty certpath>"
|
||||
}
|
||||
val certs = certPath.map {
|
||||
val bcCert = it.toBc()
|
||||
val subject = bcCert.subject.toString()
|
||||
val issuer = bcCert.issuer.toString()
|
||||
val keyIdentifier = try {
|
||||
SubjectKeyIdentifier.getInstance(bcCert.getExtension(Extension.subjectKeyIdentifier).parsedValue).keyIdentifier.toHex()
|
||||
} catch (ex: Exception) {
|
||||
"null"
|
||||
}
|
||||
val authorityKeyIdentifier = try {
|
||||
AuthorityKeyIdentifier.getInstance(bcCert.getExtension(Extension.authorityKeyIdentifier).parsedValue).keyIdentifier.toHex()
|
||||
} catch (ex: Exception) {
|
||||
"null"
|
||||
}
|
||||
" $subject[$keyIdentifier] issued by $issuer[$authorityKeyIdentifier] [${it.distributionPointsToString()}]"
|
||||
}
|
||||
return certs.joinToString("\r\n")
|
||||
}
|
||||
|
||||
private fun certPathToStringFull(chain: Array<out X509Certificate>?): String {
|
||||
if (chain == null) {
|
||||
return "<empty certpath>"
|
||||
|
@ -199,6 +199,7 @@ class AMQPBridgeTest {
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail
|
||||
doReturn(false).whenever(it).crlCheckArtemisServer
|
||||
doReturn(artemisAddress).whenever(it).p2pAddress
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
}
|
||||
|
@ -29,7 +29,10 @@ import net.corda.coretesting.internal.DEV_INTERMEDIATE_CA
|
||||
import net.corda.coretesting.internal.DEV_ROOT_CA
|
||||
import net.corda.coretesting.internal.rigorousMock
|
||||
import net.corda.coretesting.internal.stubs.CertificateStoreStubs
|
||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig
|
||||
import org.apache.activemq.artemis.api.core.RoutingType
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.*
|
||||
@ -626,4 +629,91 @@ class CertificateRevocationListNodeTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createArtemisServerAndClient(port: Int, crlCheckSoftFail: Boolean, crlCheckArtemisServer: Boolean):
|
||||
Pair<ArtemisMessagingServer, ArtemisMessagingClient> {
|
||||
val baseDirectory = temporaryFolder.root.toPath() / "artemis"
|
||||
val certificatesDirectory = baseDirectory / "certificates"
|
||||
val signingCertificateStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory)
|
||||
val p2pSslConfiguration = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
|
||||
val artemisConfig = rigorousMock<AbstractNodeConfiguration>().also {
|
||||
doReturn(baseDirectory).whenever(it).baseDirectory
|
||||
doReturn(certificatesDirectory).whenever(it).certificatesDirectory
|
||||
doReturn(CHARLIE_NAME).whenever(it).myLegalName
|
||||
doReturn(signingCertificateStore).whenever(it).signingCertificateStore
|
||||
doReturn(p2pSslConfiguration).whenever(it).p2pSslOptions
|
||||
doReturn(NetworkHostAndPort("0.0.0.0", port)).whenever(it).p2pAddress
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
doReturn(crlCheckSoftFail).whenever(it).crlCheckSoftFail
|
||||
doReturn(crlCheckArtemisServer).whenever(it).crlCheckArtemisServer
|
||||
}
|
||||
artemisConfig.configureWithDevSSLCertificate()
|
||||
val server = ArtemisMessagingServer(artemisConfig, artemisConfig.p2pAddress, MAX_MESSAGE_SIZE)
|
||||
val client = ArtemisMessagingClient(artemisConfig.p2pSslOptions, artemisConfig.p2pAddress, MAX_MESSAGE_SIZE)
|
||||
server.start()
|
||||
client.start()
|
||||
return server to client
|
||||
}
|
||||
|
||||
private fun verifyMessageToArtemis(crlCheckSoftFail: Boolean,
|
||||
crlCheckArtemisServer: Boolean,
|
||||
expectedStatus: MessageStatus,
|
||||
revokedNodeCert: Boolean = false,
|
||||
nodeCrlDistPoint: String = "http://${server.hostAndPort}/crl/node.crl") {
|
||||
val queueName = P2P_PREFIX + "Test"
|
||||
val (artemisServer, artemisClient) = createArtemisServerAndClient(serverPort, crlCheckSoftFail, crlCheckArtemisServer)
|
||||
artemisServer.use {
|
||||
artemisClient.started!!.session.createQueue(queueName, RoutingType.ANYCAST, queueName, true)
|
||||
|
||||
val (amqpClient, nodeCert) = createClient(serverPort, true, nodeCrlDistPoint)
|
||||
if (revokedNodeCert) {
|
||||
revokedNodeCerts.add(nodeCert.serialNumber)
|
||||
}
|
||||
amqpClient.use {
|
||||
val clientConnected = amqpClient.onConnection.toFuture()
|
||||
amqpClient.start()
|
||||
val clientConnect = clientConnected.get()
|
||||
assertEquals(true, clientConnect.connected)
|
||||
|
||||
val msg = amqpClient.createMessage("Test".toByteArray(), queueName, CHARLIE_NAME.toString(), emptyMap())
|
||||
amqpClient.write(msg)
|
||||
assertEquals(expectedStatus, msg.onComplete.get())
|
||||
}
|
||||
artemisClient.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `Artemis server connection succeeds with soft fail CRL check`() {
|
||||
verifyMessageToArtemis(crlCheckSoftFail = true, crlCheckArtemisServer = true, expectedStatus = MessageStatus.Acknowledged)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `Artemis server connection succeeds with hard fail CRL check`() {
|
||||
verifyMessageToArtemis(crlCheckSoftFail = false, crlCheckArtemisServer = true, expectedStatus = MessageStatus.Acknowledged)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `Artemis server connection succeeds with soft fail CRL check on unavailable URL`() {
|
||||
verifyMessageToArtemis(crlCheckSoftFail = true, crlCheckArtemisServer = true, expectedStatus = MessageStatus.Acknowledged,
|
||||
nodeCrlDistPoint = "http://${server.hostAndPort}/crl/$FORBIDDEN_CRL")
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `Artemis server connection fails with hard fail CRL check on unavailable URL`() {
|
||||
verifyMessageToArtemis(crlCheckSoftFail = false, crlCheckArtemisServer = true, expectedStatus = MessageStatus.Rejected,
|
||||
nodeCrlDistPoint = "http://${server.hostAndPort}/crl/$FORBIDDEN_CRL")
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `Artemis server connection fails with soft fail CRL check on revoked node certificate`() {
|
||||
verifyMessageToArtemis(crlCheckSoftFail = true, crlCheckArtemisServer = true, expectedStatus = MessageStatus.Rejected,
|
||||
revokedNodeCert = true)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `Artemis server connection succeeds with disabled CRL check on revoked node certificate`() {
|
||||
verifyMessageToArtemis(crlCheckSoftFail = false, crlCheckArtemisServer = false, expectedStatus = MessageStatus.Acknowledged,
|
||||
revokedNodeCert = true)
|
||||
}
|
||||
}
|
||||
|
@ -424,6 +424,7 @@ class ProtonWrapperTests {
|
||||
doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
doReturn(true).whenever(it).crlCheckSoftFail
|
||||
doReturn(true).whenever(it).crlCheckArtemisServer
|
||||
}
|
||||
artemisConfig.configureWithDevSSLCertificate()
|
||||
|
||||
|
@ -87,6 +87,8 @@ class ArtemisMessagingTest {
|
||||
doReturn(NetworkHostAndPort("0.0.0.0", serverPort)).whenever(it).p2pAddress
|
||||
doReturn(null).whenever(it).jmxMonitoringHttpPort
|
||||
doReturn(FlowTimeoutConfiguration(5.seconds, 3, backoffBase = 1.0)).whenever(it).flowTimeout
|
||||
doReturn(true).whenever(it).crlCheckSoftFail
|
||||
doReturn(true).whenever(it).crlCheckArtemisServer
|
||||
}
|
||||
LogHelper.setLevel(PersistentUniquenessProvider::class)
|
||||
database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null })
|
||||
|
@ -7,6 +7,7 @@ import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.services.rpc.LoginListener
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
|
||||
@ -138,6 +139,8 @@ class BrokerJaasLoginModule : BaseBrokerJaasLoginModule() {
|
||||
requireTls(certificates)
|
||||
// This check is redundant as it was performed already during the SSL handshake
|
||||
CertificateChainCheckPolicy.RootMustMatch.createCheck(p2pJaasConfig!!.keyStore, p2pJaasConfig!!.trustStore).checkCertificateChain(certificates!!)
|
||||
CertificateChainCheckPolicy.RevocationCheck(p2pJaasConfig!!.revocationMode)
|
||||
.createCheck(p2pJaasConfig!!.keyStore, p2pJaasConfig!!.trustStore).checkCertificateChain(certificates)
|
||||
Pair(certificates.first().subjectDN.name, listOf(RolePrincipal(PEER_ROLE)))
|
||||
}
|
||||
else -> {
|
||||
@ -161,7 +164,7 @@ data class RPCJaasConfig(
|
||||
val loginListener: LoginListener, //callback that dynamically assigns security roles to RPC users on their authentication
|
||||
val useSslForRPC: Boolean)
|
||||
|
||||
data class P2PJaasConfig(val keyStore: KeyStore, val trustStore: KeyStore)
|
||||
data class P2PJaasConfig(val keyStore: KeyStore, val trustStore: KeyStore, val revocationMode: RevocationConfig.Mode)
|
||||
|
||||
data class NodeJaasConfig(val keyStore: KeyStore, val trustStore: KeyStore)
|
||||
|
||||
|
@ -1,11 +1,25 @@
|
||||
package net.corda.node.internal.artemis
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.certPathToString
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.CertPathValidator
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.PKIXBuilderParameters
|
||||
import java.security.cert.PKIXRevocationChecker
|
||||
import java.security.cert.X509CertSelector
|
||||
import java.util.EnumSet
|
||||
|
||||
sealed class CertificateChainCheckPolicy {
|
||||
companion object {
|
||||
val log = contextLogger()
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface Check {
|
||||
@Suppress("DEPRECATION") // should use java.security.cert.X509Certificate
|
||||
@ -84,4 +98,45 @@ sealed class CertificateChainCheckPolicy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RevocationCheck(val revocationMode: RevocationConfig.Mode) : CertificateChainCheckPolicy() {
|
||||
override fun createCheck(keyStore: KeyStore, trustStore: KeyStore): Check {
|
||||
return object : Check {
|
||||
@Suppress("DEPRECATION") // should use java.security.cert.X509Certificate
|
||||
override fun checkCertificateChain(theirChain: Array<javax.security.cert.X509Certificate>) {
|
||||
if (revocationMode == RevocationConfig.Mode.OFF) {
|
||||
return
|
||||
}
|
||||
// Convert javax.security.cert.X509Certificate to java.security.cert.X509Certificate.
|
||||
val chain = theirChain.map { X509CertificateFactory().generateCertificate(it.encoded.inputStream()) }
|
||||
log.info("Check Client Certpath:\r\n${certPathToString(chain.toTypedArray())}")
|
||||
|
||||
// Drop the last certificate which must be a trusted root (validated by RootMustMatch).
|
||||
// Assume that there is no more trusted roots (or corresponding public keys) in the remaining chain.
|
||||
// See PKIXValidator.engineValidate() for reference implementation.
|
||||
val certPath = X509Utilities.buildCertPath(chain.dropLast(1))
|
||||
val certPathValidator = CertPathValidator.getInstance("PKIX")
|
||||
val pkixRevocationChecker = certPathValidator.revocationChecker as PKIXRevocationChecker
|
||||
pkixRevocationChecker.options = EnumSet.of(
|
||||
// Prefer CRL over OCSP
|
||||
PKIXRevocationChecker.Option.PREFER_CRLS,
|
||||
// Don't fall back to OCSP checking
|
||||
PKIXRevocationChecker.Option.NO_FALLBACK)
|
||||
if (revocationMode == RevocationConfig.Mode.SOFT_FAIL) {
|
||||
// Allow revocation check to succeed if the revocation status cannot be determined for one of
|
||||
// the following reasons: The CRL or OCSP response cannot be obtained because of a network error.
|
||||
pkixRevocationChecker.options = pkixRevocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL
|
||||
}
|
||||
val params = PKIXBuilderParameters(trustStore, X509CertSelector())
|
||||
params.addCertPathChecker(pkixRevocationChecker)
|
||||
try {
|
||||
certPathValidator.validate(certPath, params)
|
||||
} catch (ex: CertPathValidatorException) {
|
||||
log.error("Bad certificate path", ex)
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -70,6 +70,7 @@ interface NodeConfiguration : ConfigurationWithOptionsContainer {
|
||||
val flowMonitorPeriodMillis: Duration get() = DEFAULT_FLOW_MONITOR_PERIOD_MILLIS
|
||||
val flowMonitorSuspensionLoggingThresholdMillis: Duration get() = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS
|
||||
val crlCheckSoftFail: Boolean
|
||||
val crlCheckArtemisServer: Boolean
|
||||
val jmxReporterType: JmxReporterType? get() = defaultJmxReporterType
|
||||
|
||||
val baseDirectory: Path
|
||||
|
@ -32,6 +32,7 @@ data class NodeConfigurationImpl(
|
||||
private val keyStorePassword: String,
|
||||
private val trustStorePassword: String,
|
||||
override val crlCheckSoftFail: Boolean,
|
||||
override val crlCheckArtemisServer: Boolean = Defaults.crlCheckArtemisServer,
|
||||
override val dataSourceProperties: Properties,
|
||||
override val compatibilityZoneURL: URL? = Defaults.compatibilityZoneURL,
|
||||
override var networkServices: NetworkServicesConfig? = Defaults.networkServices,
|
||||
@ -91,6 +92,7 @@ data class NodeConfigurationImpl(
|
||||
val networkServices: NetworkServicesConfig? = null
|
||||
val tlsCertCrlDistPoint: URL? = null
|
||||
val tlsCertCrlIssuer: X500Principal? = null
|
||||
const val crlCheckArtemisServer: Boolean = false
|
||||
val security: SecurityConfiguration? = null
|
||||
val additionalP2PAddresses: List<NetworkHostAndPort> = emptyList()
|
||||
val rpcAddress: NetworkHostAndPort? = null
|
||||
|
@ -47,6 +47,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
|
||||
private val flowMonitorPeriodMillis by duration().optional().withDefaultValue(Defaults.flowMonitorPeriodMillis)
|
||||
private val flowMonitorSuspensionLoggingThresholdMillis by duration().optional().withDefaultValue(Defaults.flowMonitorSuspensionLoggingThresholdMillis)
|
||||
private val crlCheckSoftFail by boolean()
|
||||
private val crlCheckArtemisServer by boolean().optional().withDefaultValue(Defaults.crlCheckArtemisServer)
|
||||
private val jmxReporterType by enum(JmxReporterType::class).optional().withDefaultValue(Defaults.jmxReporterType)
|
||||
private val baseDirectory by string().mapValid(::toPath)
|
||||
private val flowOverrides by nested(FlowOverridesConfigSpec).optional()
|
||||
@ -86,6 +87,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
|
||||
keyStorePassword = config[keyStorePassword],
|
||||
trustStorePassword = config[trustStorePassword],
|
||||
crlCheckSoftFail = config[crlCheckSoftFail],
|
||||
crlCheckArtemisServer = config[crlCheckArtemisServer],
|
||||
dataSourceProperties = config[dataSourceProperties],
|
||||
rpcUsers = config[rpcUsers],
|
||||
verifierType = config[verifierType],
|
||||
|
@ -18,6 +18,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.JOURNAL_HE
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.p2pAcceptorTcpTransport
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
|
||||
import net.corda.nodeapi.internal.requireOnDefaultFileSystem
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
@ -170,12 +171,17 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager {
|
||||
val keyStore = config.p2pSslOptions.keyStore.get().value.internal
|
||||
val trustStore = config.p2pSslOptions.trustStore.get().value.internal
|
||||
val revocationMode = when {
|
||||
config.crlCheckArtemisServer && config.crlCheckSoftFail -> RevocationConfig.Mode.SOFT_FAIL
|
||||
config.crlCheckArtemisServer && !config.crlCheckSoftFail -> RevocationConfig.Mode.HARD_FAIL
|
||||
else -> RevocationConfig.Mode.OFF
|
||||
}
|
||||
|
||||
val securityConfig = object : SecurityConfiguration() {
|
||||
// Override to make it work with our login module
|
||||
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
|
||||
val options = mapOf(
|
||||
BrokerJaasLoginModule.P2P_SECURITY_CONFIG to P2PJaasConfig(keyStore, trustStore),
|
||||
BrokerJaasLoginModule.P2P_SECURITY_CONFIG to P2PJaasConfig(keyStore, trustStore, revocationMode),
|
||||
BrokerJaasLoginModule.NODE_SECURITY_CONFIG to NodeJaasConfig(keyStore, trustStore)
|
||||
)
|
||||
return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
|
||||
|
@ -0,0 +1,214 @@
|
||||
package net.corda.node.internal.artemis
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.node.internal.artemis.CertificateChainCheckPolicy.RevocationCheck
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.nodeapi.internal.protonwrapper.netty.RevocationConfig
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.CRLReason
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import org.bouncycastle.asn1.x509.ExtensionsGenerator
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralNames
|
||||
import org.bouncycastle.asn1.x509.IssuingDistributionPoint
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.io.File
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import javax.security.auth.x500.X500Principal
|
||||
import kotlin.test.assertFails
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class RevocationCheckTest(private val revocationMode: RevocationConfig.Mode) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "revocationMode = {0}")
|
||||
fun data() = listOf(RevocationConfig.Mode.OFF, RevocationConfig.Mode.SOFT_FAIL, RevocationConfig.Mode.HARD_FAIL)
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
private lateinit var rootCRL: File
|
||||
private lateinit var doormanCRL: File
|
||||
private lateinit var tlsCRL: File
|
||||
|
||||
private val keyStore = KeyStore.getInstance("JKS")
|
||||
private val trustStore = KeyStore.getInstance("JKS")
|
||||
|
||||
private val rootKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
private val tlsCRLIssuerKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
private val doormanKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
private val nodeCAKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
private val tlsKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
|
||||
private lateinit var rootCert: X509Certificate
|
||||
private lateinit var tlsCRLIssuerCert: X509Certificate
|
||||
private lateinit var doormanCert: X509Certificate
|
||||
private lateinit var nodeCACert: X509Certificate
|
||||
private lateinit var tlsCert: X509Certificate
|
||||
|
||||
private val chain
|
||||
get() = listOf(tlsCert, nodeCACert, doormanCert, rootCert).map {
|
||||
javax.security.cert.X509Certificate.getInstance(it.encoded)
|
||||
}.toTypedArray()
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
rootCRL = tempFolder.newFile("root.crl")
|
||||
doormanCRL = tempFolder.newFile("doorman.crl")
|
||||
tlsCRL = tempFolder.newFile("tls.crl")
|
||||
|
||||
rootCert = X509Utilities.createSelfSignedCACertificate(X500Principal("CN=root"), rootKeyPair)
|
||||
tlsCRLIssuerCert = X509Utilities.createSelfSignedCACertificate(X500Principal("CN=issuer"), tlsCRLIssuerKeyPair)
|
||||
|
||||
trustStore.load(null, null)
|
||||
trustStore.setCertificateEntry("cordatlscrlsigner", tlsCRLIssuerCert)
|
||||
trustStore.setCertificateEntry("cordarootca", rootCert)
|
||||
|
||||
doormanCert = X509Utilities.createCertificate(
|
||||
CertificateType.INTERMEDIATE_CA, rootCert, rootKeyPair, X500Principal("CN=doorman"), doormanKeyPair.public,
|
||||
crlDistPoint = rootCRL.toURI().toString()
|
||||
)
|
||||
nodeCACert = X509Utilities.createCertificate(
|
||||
CertificateType.NODE_CA, doormanCert, doormanKeyPair, X500Principal("CN=node"), nodeCAKeyPair.public,
|
||||
crlDistPoint = doormanCRL.toURI().toString()
|
||||
)
|
||||
tlsCert = X509Utilities.createCertificate(
|
||||
CertificateType.TLS, nodeCACert, nodeCAKeyPair, X500Principal("CN=tls"), tlsKeyPair.public,
|
||||
crlDistPoint = tlsCRL.toURI().toString(), crlIssuer = X500Name.getInstance(tlsCRLIssuerCert.issuerX500Principal.encoded)
|
||||
)
|
||||
|
||||
rootCRL.createCRL(rootCert, rootKeyPair.private, false)
|
||||
doormanCRL.createCRL(doormanCert, doormanKeyPair.private, false)
|
||||
tlsCRL.createCRL(tlsCRLIssuerCert, tlsCRLIssuerKeyPair.private, true)
|
||||
}
|
||||
|
||||
private fun File.createCRL(certificate: X509Certificate, privateKey: PrivateKey, indirect: Boolean, vararg revoked: X509Certificate) {
|
||||
val builder = JcaX509v2CRLBuilder(certificate.subjectX500Principal, Date())
|
||||
builder.setNextUpdate(Date.from(Date().toInstant() + 7.days))
|
||||
builder.addExtension(Extension.issuingDistributionPoint, true, IssuingDistributionPoint(null, indirect, false))
|
||||
revoked.forEach {
|
||||
val extensionsGenerator = ExtensionsGenerator()
|
||||
extensionsGenerator.addExtension(Extension.reasonCode, false, CRLReason.lookup(CRLReason.keyCompromise))
|
||||
// Certificate issuer is required for indirect CRL
|
||||
val certificateIssuerName = X500Name.getInstance(it.issuerX500Principal.encoded)
|
||||
extensionsGenerator.addExtension(Extension.certificateIssuer, true, GeneralNames(GeneralName(certificateIssuerName)))
|
||||
builder.addCRLEntry(it.serialNumber, Date(), extensionsGenerator.generate())
|
||||
}
|
||||
val holder = builder.build(JcaContentSignerBuilder("SHA256withECDSA").setProvider(Crypto.findProvider("BC")).build(privateKey))
|
||||
outputStream().use { it.write(holder.encoded) }
|
||||
}
|
||||
|
||||
private fun assertFailsFor(vararg modes: RevocationConfig.Mode, block: () -> Unit) {
|
||||
if (revocationMode in modes) assertFails(block) else block()
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `ok with empty CRLs`() {
|
||||
RevocationCheck(revocationMode).createCheck(keyStore, trustStore).checkCertificateChain(chain)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `soft fail with revoked TLS certificate`() {
|
||||
tlsCRL.createCRL(tlsCRLIssuerCert, tlsCRLIssuerKeyPair.private, true, tlsCert)
|
||||
|
||||
assertFailsFor(RevocationConfig.Mode.SOFT_FAIL, RevocationConfig.Mode.HARD_FAIL) {
|
||||
RevocationCheck(revocationMode).createCheck(keyStore, trustStore).checkCertificateChain(chain)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `hard fail with unavailable CRL in TLS certificate`() {
|
||||
tlsCert = X509Utilities.createCertificate(
|
||||
CertificateType.TLS, nodeCACert, nodeCAKeyPair, X500Principal("CN=tls"), tlsKeyPair.public,
|
||||
crlDistPoint = "http://unknown-host:10000/certificate-revocation-list/tls",
|
||||
crlIssuer = X500Name.getInstance(tlsCRLIssuerCert.issuerX500Principal.encoded)
|
||||
)
|
||||
|
||||
assertFailsFor(RevocationConfig.Mode.HARD_FAIL) {
|
||||
RevocationCheck(revocationMode).createCheck(keyStore, trustStore).checkCertificateChain(chain)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `hard fail with invalid CRL issuer in TLS certificate`() {
|
||||
tlsCert = X509Utilities.createCertificate(
|
||||
CertificateType.TLS, nodeCACert, nodeCAKeyPair, X500Principal("CN=tls"), tlsKeyPair.public,
|
||||
crlDistPoint = tlsCRL.toURI().toString(), crlIssuer = X500Name("CN=unknown")
|
||||
)
|
||||
|
||||
assertFailsFor(RevocationConfig.Mode.HARD_FAIL) {
|
||||
RevocationCheck(revocationMode).createCheck(keyStore, trustStore).checkCertificateChain(chain)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `hard fail without CRL issuer in TLS certificate`() {
|
||||
tlsCert = X509Utilities.createCertificate(
|
||||
CertificateType.TLS, nodeCACert, nodeCAKeyPair, X500Principal("CN=tls"), tlsKeyPair.public,
|
||||
crlDistPoint = tlsCRL.toURI().toString()
|
||||
)
|
||||
|
||||
assertFailsFor(RevocationConfig.Mode.HARD_FAIL) {
|
||||
RevocationCheck(revocationMode).createCheck(keyStore, trustStore).checkCertificateChain(chain)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `ok with other certificate in TLS CRL`() {
|
||||
val otherKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
val otherCert = X509Utilities.createCertificate(
|
||||
CertificateType.TLS, nodeCACert, nodeCAKeyPair, X500Principal("CN=other"), otherKeyPair.public,
|
||||
crlDistPoint = tlsCRL.toURI().toString(), crlIssuer = X500Name.getInstance(tlsCRLIssuerCert.issuerX500Principal.encoded)
|
||||
)
|
||||
tlsCRL.createCRL(tlsCRLIssuerCert, tlsCRLIssuerKeyPair.private, true, otherCert)
|
||||
|
||||
RevocationCheck(revocationMode).createCheck(keyStore, trustStore).checkCertificateChain(chain)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `soft fail with revoked node CA certificate`() {
|
||||
doormanCRL.createCRL(doormanCert, doormanKeyPair.private, false, nodeCACert)
|
||||
|
||||
assertFailsFor(RevocationConfig.Mode.SOFT_FAIL, RevocationConfig.Mode.HARD_FAIL) {
|
||||
RevocationCheck(revocationMode).createCheck(keyStore, trustStore).checkCertificateChain(chain)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `hard fail with unavailable CRL in node CA certificate`() {
|
||||
nodeCACert = X509Utilities.createCertificate(
|
||||
CertificateType.NODE_CA, doormanCert, doormanKeyPair, X500Principal("CN=node"), nodeCAKeyPair.public,
|
||||
crlDistPoint = "http://unknown-host:10000/certificate-revocation-list/doorman"
|
||||
)
|
||||
|
||||
assertFailsFor(RevocationConfig.Mode.HARD_FAIL) {
|
||||
RevocationCheck(revocationMode).createCheck(keyStore, trustStore).checkCertificateChain(chain)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `ok with other certificate in doorman CRL`() {
|
||||
val otherKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
val otherCert = X509Utilities.createCertificate(
|
||||
CertificateType.NODE_CA, doormanCert, doormanKeyPair, X500Principal("CN=other"), otherKeyPair.public,
|
||||
crlDistPoint = doormanCRL.toURI().toString()
|
||||
)
|
||||
doormanCRL.createCRL(doormanCert, doormanKeyPair.private, false, otherCert)
|
||||
|
||||
RevocationCheck(revocationMode).createCheck(keyStore, trustStore).checkCertificateChain(chain)
|
||||
}
|
||||
}
|
@ -276,6 +276,13 @@ class NodeConfigurationImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `check crlCheckArtemisServer flag`() {
|
||||
assertFalse(getConfig("working-config.conf").parseAsNodeConfiguration().value().crlCheckArtemisServer)
|
||||
val rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("crlCheckArtemisServer" to true)))
|
||||
assertTrue(rawConfig.parseAsNodeConfiguration().value().crlCheckArtemisServer)
|
||||
}
|
||||
|
||||
private fun configDebugOptions(devMode: Boolean, devModeOptions: DevModeOptions?): NodeConfigurationImpl {
|
||||
return testConfiguration.copy(devMode = devMode, devModeOptions = devModeOptions)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user