mirror of
https://github.com/corda/corda.git
synced 2025-06-19 15:43:52 +00:00
Bogdan ent 2295 SNI (#1435)
* ENT-2295: added SNI support to bridge * ENT-2295: removed unused method args, adde new line * ENT-2295: fix checking for existing bridges * ENT-2295: fix AMQPBridgeTest(included source x500 name in messages) * ENT-2295: fix ProtonWrapperTests (added source id and only check for SNI if bridge is shared) * ENT-2295: fixed issue with artemis round robin not working when autogrouping was on * ENT-2295: adapt to use openSSL, added SNI tests * ENT-2295: server side openSSL now uses SniHandler magic * ENT-2295: service queues are not exclusive * ENT-2295: remove check for nodes sharing artemis when resolving targets * ENT-2516 SNI - Log the requested server name (if any) in the AMQPServer (#1454) * WIP * log server name in ssl handshake * big fix * handle nullable sslParameters * ENT-2295: address PR comments * ENT-2295: remove unused imports * ENT-2295: fix warnings * ENT-2295: address PR comments * ENT-2295: added node to node intergration tests, added openssl dep to bridge capsule * ENT-2295: message group id is unique for service queues * ENT-2295: address PR comment
This commit is contained in:
@ -42,11 +42,12 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
private val lock = ReentrantLock()
|
||||
private val queueNamesToBridgesMap = mutableMapOf<String, MutableList<AMQPBridge>>()
|
||||
|
||||
private class AMQPConfigurationImpl private constructor(override val keyStore: CertificateStore,
|
||||
override val trustStore: CertificateStore,
|
||||
override val socksProxyConfig: SocksProxyConfig?,
|
||||
override val maxMessageSize: Int,
|
||||
override val useOpenSsl: Boolean) : AMQPConfiguration {
|
||||
private class AMQPConfigurationImpl (override val keyStore: CertificateStore,
|
||||
override val trustStore: CertificateStore,
|
||||
override val socksProxyConfig: SocksProxyConfig?,
|
||||
override val maxMessageSize: Int,
|
||||
override val useOpenSsl: Boolean,
|
||||
override val sourceX500Name: String? = null) : AMQPConfiguration {
|
||||
constructor(config: MutualSslConfiguration, socksProxyConfig: SocksProxyConfig?, maxMessageSize: Int) : this(config.keyStore.get(),
|
||||
config.trustStore.get(),
|
||||
socksProxyConfig,
|
||||
@ -72,7 +73,8 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
* If the delivery fails the session is rolled back to prevent loss of the message. This may cause duplicate delivery,
|
||||
* however Artemis and the remote Corda instanced will deduplicate these messages.
|
||||
*/
|
||||
private class AMQPBridge(val queueName: String,
|
||||
private class AMQPBridge(val sourceX500Name: String,
|
||||
val queueName: String,
|
||||
val targets: List<NetworkHostAndPort>,
|
||||
val legalNames: Set<CordaX500Name>,
|
||||
private val amqpConfig: AMQPConfiguration,
|
||||
@ -87,6 +89,7 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
val oldMDC = MDC.getCopyOfContextMap()
|
||||
try {
|
||||
MDC.put("queueName", queueName)
|
||||
MDC.put("source", amqpConfig.sourceX500Name)
|
||||
MDC.put("targets", targets.joinToString(separator = ";") { it.toString() })
|
||||
MDC.put("legalNames", legalNames.joinToString(separator = ";") { it.toString() })
|
||||
MDC.put("maxMessageSize", amqpConfig.maxMessageSize.toString())
|
||||
@ -150,7 +153,8 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
val sessionFactory = artemis.started!!.sessionFactory
|
||||
val session = sessionFactory.createSession(NODE_P2P_USER, NODE_P2P_USER, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
|
||||
this.session = session
|
||||
val consumer = session.createConsumer(queueName)
|
||||
// Several producers (in the case of shared bridge) can put messages in the same outbound p2p queue. The consumers are created using the source x500 name as a filter
|
||||
val consumer = session.createConsumer(queueName, "hyphenated_props:sender-subject-name = '${amqpConfig.sourceX500Name}'")
|
||||
this.consumer = consumer
|
||||
consumer.setMessageHandler(this@AMQPBridge::clientArtemisMessageHandler)
|
||||
session.start()
|
||||
@ -219,15 +223,16 @@ class AMQPBridgeManager(config: MutualSslConfiguration, socksProxyConfig: SocksP
|
||||
}
|
||||
}
|
||||
|
||||
override fun deployBridge(queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) {
|
||||
override fun deployBridge(sourceX500Name: String, queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>) {
|
||||
val newBridge = lock.withLock {
|
||||
val bridges = queueNamesToBridgesMap.getOrPut(queueName) { mutableListOf() }
|
||||
for (target in targets) {
|
||||
if (bridges.any { it.targets.contains(target) }) {
|
||||
if (bridges.any { it.targets.contains(target) && it.sourceX500Name == sourceX500Name }) {
|
||||
return
|
||||
}
|
||||
}
|
||||
val newBridge = AMQPBridge(queueName, targets, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!, bridgeMetricsService)
|
||||
val newAMQPConfig = AMQPConfigurationImpl(amqpConfig.keyStore, amqpConfig.trustStore, amqpConfig.socksProxyConfig, amqpConfig.maxMessageSize, amqpConfig.useOpenSsl, sourceX500Name)
|
||||
val newBridge = AMQPBridge(sourceX500Name, queueName, targets, legalNames, newAMQPConfig, sharedEventLoopGroup!!, artemis!!, bridgeMetricsService)
|
||||
bridges += newBridge
|
||||
bridgeMetricsService?.bridgeCreated(targets, legalNames)
|
||||
newBridge
|
||||
|
@ -159,7 +159,7 @@ class BridgeControlListener(val config: MutualSslConfiguration,
|
||||
return
|
||||
}
|
||||
for (outQueue in controlMessage.sendQueues) {
|
||||
bridgeManager.deployBridge(outQueue.queueName, outQueue.targets, outQueue.legalNames.toSet())
|
||||
bridgeManager.deployBridge(controlMessage.nodeIdentity, outQueue.queueName, outQueue.targets, outQueue.legalNames.toSet())
|
||||
}
|
||||
val wasActive = active
|
||||
validInboundQueues.addAll(controlMessage.inboxQueues)
|
||||
@ -175,7 +175,7 @@ class BridgeControlListener(val config: MutualSslConfiguration,
|
||||
log.error("Invalid queue names in control message $controlMessage")
|
||||
return
|
||||
}
|
||||
bridgeManager.deployBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets, controlMessage.bridgeInfo.legalNames.toSet())
|
||||
bridgeManager.deployBridge(controlMessage.nodeIdentity, controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets, controlMessage.bridgeInfo.legalNames.toSet())
|
||||
}
|
||||
is BridgeControl.Delete -> {
|
||||
if (!controlMessage.bridgeInfo.queueName.startsWith(PEERS_PREFIX)) {
|
||||
|
@ -9,7 +9,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
*/
|
||||
@VisibleForTesting
|
||||
interface BridgeManager : AutoCloseable {
|
||||
fun deployBridge(queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
|
||||
fun deployBridge(sourceX500Name: String, queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
|
||||
|
||||
fun destroyBridge(queueName: String, targets: List<NetworkHostAndPort>)
|
||||
|
||||
|
@ -59,6 +59,7 @@ interface CertificateStore : Iterable<Pair<String, X509Certificate>> {
|
||||
forEach { (alias, certificate) -> action.invoke(alias, certificate) }
|
||||
}
|
||||
|
||||
fun aliases(): List<String> = value.internal.aliases().toList()
|
||||
/**
|
||||
* @throws IllegalArgumentException if no certificate for the alias is found, or if the certificate is not an [X509Certificate].
|
||||
*/
|
||||
|
@ -7,6 +7,7 @@ import io.netty.channel.ChannelPromise
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.handler.proxy.ProxyConnectException
|
||||
import io.netty.handler.proxy.ProxyConnectionEvent
|
||||
import io.netty.handler.ssl.SniCompletionEvent
|
||||
import io.netty.handler.ssl.SslHandler
|
||||
import io.netty.handler.ssl.SslHandshakeCompletionEvent
|
||||
import io.netty.util.ReferenceCountUtil
|
||||
@ -25,6 +26,8 @@ import org.slf4j.MDC
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.channels.ClosedChannelException
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.ExtendedSSLSession
|
||||
import javax.net.ssl.SNIHostName
|
||||
import javax.net.ssl.SSLException
|
||||
|
||||
/**
|
||||
@ -34,7 +37,7 @@ import javax.net.ssl.SSLException
|
||||
*/
|
||||
internal class AMQPChannelHandler(private val serverMode: Boolean,
|
||||
private val allowedRemoteLegalNames: Set<CordaX500Name>?,
|
||||
private var keyManagerFactory: CertHoldingKeyManagerFactoryWrapper,
|
||||
private val keyManagerFactoriesMap: Map<String, CertHoldingKeyManagerFactoryWrapper>,
|
||||
private val userName: String?,
|
||||
private val password: String?,
|
||||
private val trace: Boolean,
|
||||
@ -51,6 +54,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
|
||||
private var suppressClose: Boolean = false
|
||||
private var badCert: Boolean = false
|
||||
private var localCert: X509Certificate? = null
|
||||
private var requestedServerName: String? = null
|
||||
|
||||
private fun withMDC(block: () -> Unit) {
|
||||
val oldMDC = MDC.getCopyOfContextMap()
|
||||
@ -117,58 +121,43 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
|
||||
}
|
||||
|
||||
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
||||
if (evt is ProxyConnectionEvent) {
|
||||
remoteAddress = evt.destinationAddress() // update address to teh real target address
|
||||
}
|
||||
if (evt is SslHandshakeCompletionEvent) {
|
||||
if (evt.isSuccess) {
|
||||
val sslHandler = ctx.pipeline().get(SslHandler::class.java)
|
||||
val sslSession = sslHandler.engine().session
|
||||
localCert = keyManagerFactory.getCurrentCertChain()?.get(0)
|
||||
if (localCert == null) {
|
||||
log.error("SSL KeyManagerFactory failed to provide a local cert")
|
||||
ctx.close()
|
||||
return
|
||||
}
|
||||
if (sslSession.peerCertificates == null || sslSession.peerCertificates.isEmpty()) {
|
||||
log.error("No peer certificates")
|
||||
ctx.close()
|
||||
return
|
||||
}
|
||||
remoteCert = sslHandler.engine().session.peerCertificates[0].x509
|
||||
val remoteX500Name = try {
|
||||
CordaX500Name.build(remoteCert!!.subjectX500Principal)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
badCert = true
|
||||
logErrorWithMDC("Certificate subject not a valid CordaX500Name", ex)
|
||||
ctx.close()
|
||||
return
|
||||
}
|
||||
if (allowedRemoteLegalNames != null && remoteX500Name !in allowedRemoteLegalNames) {
|
||||
badCert = true
|
||||
logErrorWithMDC("Provided certificate subject $remoteX500Name not in expected set $allowedRemoteLegalNames")
|
||||
ctx.close()
|
||||
return
|
||||
}
|
||||
logInfoWithMDC("Handshake completed with subject: $remoteX500Name")
|
||||
createAMQPEngine(ctx)
|
||||
onOpen(Pair(ctx.channel() as SocketChannel, ConnectionChange(remoteAddress, remoteCert, true, false)))
|
||||
} else {
|
||||
val cause = evt.cause()
|
||||
// This happens when the peer node is closed during SSL establishment.
|
||||
if (cause is ClosedChannelException) {
|
||||
logWarnWithMDC("SSL Handshake closed early.")
|
||||
} else if (cause is SSLException && cause.message == "handshake timed out") { // Sadly the exception thrown by Netty wrapper requires that we check the message.
|
||||
logWarnWithMDC("SSL Handshake timed out")
|
||||
} else {
|
||||
badCert = true
|
||||
}
|
||||
logErrorWithMDC("Handshake failure: ${evt.cause().message}")
|
||||
if (log.isTraceEnabled) {
|
||||
withMDC { log.trace("Handshake failure", evt.cause()) }
|
||||
}
|
||||
ctx.close()
|
||||
when (evt) {
|
||||
is ProxyConnectionEvent -> {
|
||||
// update address to the real target address
|
||||
remoteAddress = evt.destinationAddress()
|
||||
}
|
||||
is SniCompletionEvent -> {
|
||||
if (evt.isSuccess) {
|
||||
// The SniCompletionEvent is fired up before context is switched (after SslHandshakeCompletionEvent)
|
||||
// so we save the requested server name now to be able log it once the handshake is completed successfully
|
||||
// Note: this event is only triggered when using OpenSSL.
|
||||
requestedServerName = evt.hostname()
|
||||
logInfoWithMDC("SNI completion success.")
|
||||
} else {
|
||||
logErrorWithMDC("SNI completion failure: ${evt.cause().message}")
|
||||
}
|
||||
}
|
||||
is SslHandshakeCompletionEvent -> {
|
||||
if (evt.isSuccess) {
|
||||
handleSuccessfulHandshake(ctx)
|
||||
} else {
|
||||
handleFailedHandshake(ctx, evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SslHandler.getRequestedServerName(): String? {
|
||||
return if (serverMode) {
|
||||
val session = engine().session
|
||||
when (session) {
|
||||
// Server name can be obtained from SSL session when using JavaSSL.
|
||||
is ExtendedSSLSession -> (session.requestedServerNames.firstOrNull() as? SNIHostName)?.asciiName
|
||||
// For Open SSL server name is obtained from SniCompletionEvent
|
||||
else -> requestedServerName
|
||||
}
|
||||
} else {
|
||||
(engine().sslParameters?.serverNames?.firstOrNull() as? SNIHostName)?.asciiName
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,4 +223,62 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
|
||||
}
|
||||
eventProcessor!!.processEventsAsync()
|
||||
}
|
||||
|
||||
private fun handleSuccessfulHandshake(ctx: ChannelHandlerContext) {
|
||||
val sslHandler = ctx.pipeline().get(SslHandler::class.java)
|
||||
val sslSession = sslHandler.engine().session
|
||||
// Depending on what matching method is used, getting the local certificate is done by selecting the
|
||||
// appropriate keyManagerFactory
|
||||
val keyManagerFactory = requestedServerName?.let {
|
||||
keyManagerFactoriesMap[it]
|
||||
} ?: keyManagerFactoriesMap.values.single()
|
||||
|
||||
localCert = keyManagerFactory.getCurrentCertChain()?.first()
|
||||
|
||||
if (localCert == null) {
|
||||
log.error("SSL KeyManagerFactory failed to provide a local cert")
|
||||
ctx.close()
|
||||
return
|
||||
}
|
||||
if (sslSession.peerCertificates == null || sslSession.peerCertificates.isEmpty()) {
|
||||
log.error("No peer certificates")
|
||||
ctx.close()
|
||||
return
|
||||
}
|
||||
remoteCert = sslHandler.engine().session.peerCertificates.first().x509
|
||||
val remoteX500Name = try {
|
||||
CordaX500Name.build(remoteCert!!.subjectX500Principal)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
badCert = true
|
||||
logErrorWithMDC("Certificate subject not a valid CordaX500Name", ex)
|
||||
ctx.close()
|
||||
return
|
||||
}
|
||||
if (allowedRemoteLegalNames != null && remoteX500Name !in allowedRemoteLegalNames) {
|
||||
badCert = true
|
||||
logErrorWithMDC("Provided certificate subject $remoteX500Name not in expected set $allowedRemoteLegalNames")
|
||||
ctx.close()
|
||||
return
|
||||
}
|
||||
|
||||
logInfoWithMDC("Handshake completed with subject: $remoteX500Name, requested server name: ${sslHandler.getRequestedServerName()}.")
|
||||
createAMQPEngine(ctx)
|
||||
onOpen(Pair(ctx.channel() as SocketChannel, ConnectionChange(remoteAddress, remoteCert, true, false)))
|
||||
}
|
||||
|
||||
private fun handleFailedHandshake(ctx: ChannelHandlerContext, evt: SslHandshakeCompletionEvent) {
|
||||
val cause = evt.cause()
|
||||
// This happens when the peer node is closed during SSL establishment.
|
||||
when {
|
||||
cause is ClosedChannelException -> logWarnWithMDC("SSL Handshake closed early.")
|
||||
// Sadly the exception thrown by Netty wrapper requires that we check the message.
|
||||
cause is SSLException && cause.message == "handshake timed out" -> logWarnWithMDC("SSL Handshake timed out")
|
||||
else -> badCert = true
|
||||
}
|
||||
logErrorWithMDC("Handshake failure: ${evt.cause().message}")
|
||||
if (log.isTraceEnabled) {
|
||||
withMDC { log.trace("Handshake failure", evt.cause()) }
|
||||
}
|
||||
ctx.close()
|
||||
}
|
||||
}
|
@ -14,14 +14,18 @@ import io.netty.util.internal.logging.Slf4JLoggerFactory
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.crypto.x509
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.ReceivedMessage
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
|
||||
import net.corda.nodeapi.internal.protonwrapper.messages.impl.SendableMessageImpl
|
||||
import net.corda.nodeapi.internal.requireMessageSize
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import sun.security.x509.X500Name
|
||||
import java.lang.Long.min
|
||||
import java.net.InetSocketAddress
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@ -158,7 +162,7 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
}
|
||||
}
|
||||
|
||||
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory)
|
||||
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory, parent.configuration)
|
||||
val target = parent.currentTarget
|
||||
val handler = if (parent.configuration.useOpenSsl){
|
||||
createClientOpenSslHandler(target, parent.allowedRemoteLegalNames, wrappedKeyManagerFactory, trustManagerFactory, ch.alloc())
|
||||
@ -169,7 +173,8 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
|
||||
if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
|
||||
pipeline.addLast(AMQPChannelHandler(false,
|
||||
parent.allowedRemoteLegalNames,
|
||||
wrappedKeyManagerFactory,
|
||||
// Single entry, key can be anything.
|
||||
mapOf(DEFAULT to wrappedKeyManagerFactory),
|
||||
conf.userName,
|
||||
conf.password,
|
||||
conf.trace,
|
||||
|
@ -2,7 +2,6 @@ package net.corda.nodeapi.internal.protonwrapper.netty
|
||||
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import java.security.KeyStore
|
||||
|
||||
interface AMQPConfiguration {
|
||||
/**
|
||||
@ -56,6 +55,10 @@ interface AMQPConfiguration {
|
||||
val socksProxyConfig: SocksProxyConfig?
|
||||
get() = null
|
||||
|
||||
@JvmDefault
|
||||
val sourceX500Name: String?
|
||||
get() = null
|
||||
|
||||
/**
|
||||
* Whether to use the tcnative open/boring SSL provider or the default Java SSL provider
|
||||
*/
|
||||
|
@ -23,7 +23,6 @@ import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.net.BindException
|
||||
import java.net.InetSocketAddress
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
@ -66,18 +65,37 @@ class AMQPServer(val hostName: String,
|
||||
}
|
||||
|
||||
override fun initChannel(ch: SocketChannel) {
|
||||
val amqpConfiguration = parent.configuration
|
||||
val keyStore = amqpConfiguration.keyStore
|
||||
val pipeline = ch.pipeline()
|
||||
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory)
|
||||
val handler = if (parent.configuration.useOpenSsl){
|
||||
createServerOpenSslHandler(wrappedKeyManagerFactory, trustManagerFactory, ch.alloc())
|
||||
// Used for SNI matching with javaSSL.
|
||||
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(keyManagerFactory, amqpConfiguration)
|
||||
// Used to create a mapping for SNI matching with openSSL.
|
||||
val keyManagerFactoriesMap = splitKeystore(amqpConfiguration)
|
||||
val handler = if (amqpConfiguration.useOpenSsl){
|
||||
// SNI matching needed only when multiple nodes exist behind the server.
|
||||
if (keyStore.aliases().size > 1) {
|
||||
createServerSNIOpenSslHandler(keyManagerFactoriesMap, trustManagerFactory)
|
||||
} else {
|
||||
createServerOpenSslHandler(wrappedKeyManagerFactory, trustManagerFactory, ch.alloc())
|
||||
}
|
||||
} else {
|
||||
createServerSslHelper(wrappedKeyManagerFactory, trustManagerFactory)
|
||||
// For javaSSL, SNI matching is handled at key manager level.
|
||||
createServerSslHelper(keyStore, wrappedKeyManagerFactory, trustManagerFactory)
|
||||
}
|
||||
|
||||
pipeline.addLast("sslHandler", handler)
|
||||
|
||||
if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
|
||||
pipeline.addLast(AMQPChannelHandler(true,
|
||||
null,
|
||||
wrappedKeyManagerFactory,
|
||||
// Passing a mapping of legal names to key managers to be able to pick the correct one after
|
||||
// SNI completion event is fired up.
|
||||
if (keyStore.aliases().size > 1 && amqpConfiguration.useOpenSsl)
|
||||
keyManagerFactoriesMap
|
||||
else
|
||||
// Single entry, key can be anything.
|
||||
mapOf(DEFAULT to wrappedKeyManagerFactory),
|
||||
conf.userName,
|
||||
conf.password,
|
||||
conf.trace,
|
||||
|
@ -5,7 +5,7 @@ import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.*
|
||||
|
||||
|
||||
class CertHoldingKeyManagerFactorySpiWrapper(private val factorySpi: KeyManagerFactorySpi) : KeyManagerFactorySpi() {
|
||||
class CertHoldingKeyManagerFactorySpiWrapper(private val factorySpi: KeyManagerFactorySpi, private val amqpConfig: AMQPConfiguration) : KeyManagerFactorySpi() {
|
||||
override fun engineInit(keyStore: KeyStore?, password: CharArray?) {
|
||||
val engineInitMethod = KeyManagerFactorySpi::class.java.getDeclaredMethod("engineInit", KeyStore::class.java, CharArray::class.java)
|
||||
engineInitMethod.isAccessible = true
|
||||
@ -23,16 +23,32 @@ class CertHoldingKeyManagerFactorySpiWrapper(private val factorySpi: KeyManagerF
|
||||
engineGetKeyManagersMethod.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val keyManagers = engineGetKeyManagersMethod.invoke(factorySpi) as Array<KeyManager>
|
||||
return if (factorySpi is CertHoldingKeyManagerFactorySpiWrapper) keyManagers else keyManagers.mapNotNull {
|
||||
@Suppress("USELESS_CAST") // the casts to KeyManager are not useless - without them, the typed array will be of type Any
|
||||
when (it) {
|
||||
is X509ExtendedKeyManager -> AliasProvidingExtendedKeyMangerWrapper(it) as KeyManager
|
||||
is X509KeyManager -> AliasProvidingKeyMangerWrapperImpl(it) as KeyManager
|
||||
else -> null
|
||||
return if (factorySpi is CertHoldingKeyManagerFactorySpiWrapper) keyManagers else keyManagers.map {
|
||||
val aliasProvidingKeyManager = getDefaultKeyManager(it)
|
||||
// Use the SNIKeyManager if keystore has several entries and only for clients and non-openSSL servers.
|
||||
if (amqpConfig.keyStore.aliases().size > 1) {
|
||||
// Clients
|
||||
if (amqpConfig.sourceX500Name != null) {
|
||||
SNIKeyManager(aliasProvidingKeyManager as X509ExtendedKeyManager, amqpConfig)
|
||||
} else if (!amqpConfig.useOpenSsl) { // JDK SSL servers
|
||||
SNIKeyManager(aliasProvidingKeyManager as X509ExtendedKeyManager, amqpConfig)
|
||||
} else {
|
||||
aliasProvidingKeyManager
|
||||
}
|
||||
} else {
|
||||
aliasProvidingKeyManager
|
||||
}
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
private fun getDefaultKeyManager(keyManager: KeyManager): KeyManager {
|
||||
return when (keyManager) {
|
||||
is X509ExtendedKeyManager -> AliasProvidingExtendedKeyMangerWrapper(keyManager)
|
||||
is X509KeyManager -> AliasProvidingKeyMangerWrapperImpl(keyManager)
|
||||
else -> throw UnsupportedOperationException("Supported key manager types are: X509ExtendedKeyManager, X509KeyManager. Provided ${keyManager::class.java.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private val keyManagers = lazy { getKeyManagersImpl() }
|
||||
|
||||
override fun engineGetKeyManagers(): Array<KeyManager> {
|
||||
@ -46,12 +62,12 @@ class CertHoldingKeyManagerFactorySpiWrapper(private val factorySpi: KeyManagerF
|
||||
* the wrapper is not thread safe as in it will return the last used alias/cert chain and has itself no notion
|
||||
* of belonging to a certain channel.
|
||||
*/
|
||||
class CertHoldingKeyManagerFactoryWrapper(factory: KeyManagerFactory) : KeyManagerFactory(getFactorySpi(factory), factory.provider, factory.algorithm) {
|
||||
class CertHoldingKeyManagerFactoryWrapper(factory: KeyManagerFactory, amqpConfig: AMQPConfiguration) : KeyManagerFactory(getFactorySpi(factory, amqpConfig), factory.provider, factory.algorithm) {
|
||||
companion object {
|
||||
private fun getFactorySpi(factory: KeyManagerFactory): KeyManagerFactorySpi {
|
||||
private fun getFactorySpi(factory: KeyManagerFactory, amqpConfig: AMQPConfiguration): KeyManagerFactorySpi {
|
||||
val spiField = KeyManagerFactory::class.java.getDeclaredField("factorySpi")
|
||||
spiField.isAccessible = true
|
||||
return CertHoldingKeyManagerFactorySpiWrapper(spiField.get(factory) as KeyManagerFactorySpi)
|
||||
return CertHoldingKeyManagerFactorySpiWrapper(spiField.get(factory) as KeyManagerFactorySpi, amqpConfig)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,109 @@
|
||||
package net.corda.nodeapi.internal.protonwrapper.netty
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.crypto.x509
|
||||
import org.slf4j.MDC
|
||||
import sun.security.x509.X500Name
|
||||
import java.net.Socket
|
||||
import java.security.Principal
|
||||
import javax.net.ssl.*
|
||||
|
||||
internal class SNIKeyManager(private val keyManager: X509ExtendedKeyManager, private val amqpConfig: AMQPConfiguration) : X509ExtendedKeyManager(), X509KeyManager by keyManager, AliasProvidingKeyMangerWrapper {
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
override var lastAlias: String? = null
|
||||
|
||||
private fun withMDC(block: () -> Unit) {
|
||||
val oldMDC = MDC.getCopyOfContextMap()
|
||||
try {
|
||||
MDC.put("lastAlias", lastAlias)
|
||||
MDC.put("isServer", amqpConfig.sourceX500Name.isNullOrEmpty().toString())
|
||||
MDC.put("sourceX500Name", amqpConfig.sourceX500Name)
|
||||
MDC.put("useOpenSSL", amqpConfig.useOpenSsl.toString())
|
||||
block()
|
||||
} finally {
|
||||
MDC.setContextMap(oldMDC)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logDebugWithMDC(msg: () -> String) {
|
||||
if (log.isDebugEnabled) {
|
||||
withMDC { log.debug(msg()) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun chooseClientAlias(keyType: Array<out String>, issuers: Array<out Principal>, socket: Socket): String? {
|
||||
return storeIfNotNull { chooseClientAlias(amqpConfig.keyStore, amqpConfig.sourceX500Name) }
|
||||
}
|
||||
|
||||
override fun chooseEngineClientAlias(keyType: Array<out String>, issuers: Array<out Principal>, engine: SSLEngine): String? {
|
||||
return storeIfNotNull { chooseClientAlias(amqpConfig.keyStore, amqpConfig.sourceX500Name) }
|
||||
}
|
||||
|
||||
override fun chooseServerAlias(keyType: String?, issuers: Array<out Principal>?, socket: Socket): String? {
|
||||
return storeIfNotNull {
|
||||
val matcher = (socket as SSLSocket).sslParameters.sniMatchers.first()
|
||||
chooseServerAlias(keyType, issuers, matcher)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chooseEngineServerAlias(keyType: String?, issuers: Array<out Principal>?, engine: SSLEngine?): String? {
|
||||
return storeIfNotNull {
|
||||
val matcher = engine?.sslParameters?.sniMatchers?.first()
|
||||
chooseServerAlias(keyType, issuers, matcher)
|
||||
}
|
||||
}
|
||||
|
||||
private fun chooseServerAlias(keyType: String?, issuers: Array<out Principal>?, matcher: SNIMatcher?): String? {
|
||||
val aliases = keyManager.getServerAliases(keyType, issuers)
|
||||
if (aliases == null || aliases.isEmpty()) {
|
||||
logDebugWithMDC { "Keystore doesn't contain any aliases for key type $keyType and issuers $issuers." }
|
||||
return null
|
||||
}
|
||||
|
||||
log.debug("Checking aliases: $aliases.")
|
||||
matcher?.let {
|
||||
val matchedAlias = (it as ServerSNIMatcher).matchedAlias
|
||||
if (aliases.contains(matchedAlias)) {
|
||||
logDebugWithMDC { "Found match for $matchedAlias." }
|
||||
return matchedAlias
|
||||
}
|
||||
}
|
||||
|
||||
logDebugWithMDC { "Unable to find a matching alias." }
|
||||
return null
|
||||
}
|
||||
|
||||
private fun chooseClientAlias(keyStore: CertificateStore, clientLegalName: String?): String? {
|
||||
clientLegalName?.let {
|
||||
val aliases = keyStore.aliases()
|
||||
if (aliases.isEmpty()) {
|
||||
logDebugWithMDC { "Keystore doesn't contain any entries." }
|
||||
}
|
||||
aliases.forEach { alias ->
|
||||
val x500Name = keyStore[alias].x509.subjectDN as X500Name
|
||||
val aliasCordaX500Name = CordaX500Name.build(x500Name.asX500Principal())
|
||||
val clientCordaX500Name = CordaX500Name.parse(it)
|
||||
if (clientCordaX500Name == aliasCordaX500Name) {
|
||||
logDebugWithMDC { "Found alias $alias for $clientCordaX500Name." }
|
||||
return alias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun storeIfNotNull(func: () -> String?): String? {
|
||||
val alias = func()
|
||||
if (alias != null) {
|
||||
lastAlias = alias
|
||||
}
|
||||
return alias
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
package net.corda.nodeapi.internal.protonwrapper.netty
|
||||
|
||||
import io.netty.buffer.ByteBufAllocator
|
||||
import io.netty.handler.ssl.SslContextBuilder
|
||||
import io.netty.handler.ssl.SslHandler
|
||||
import io.netty.handler.ssl.SslProvider
|
||||
import io.netty.handler.ssl.*
|
||||
import io.netty.util.DomainNameMappingBuilder
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -13,15 +12,19 @@ import net.corda.core.utilities.toHex
|
||||
import net.corda.nodeapi.internal.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.crypto.toBc
|
||||
import net.corda.nodeapi.internal.crypto.x509
|
||||
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier
|
||||
import sun.security.x509.X500Name
|
||||
import java.net.Socket
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.*
|
||||
import java.util.*
|
||||
import javax.net.ssl.*
|
||||
|
||||
private const val HOSTNAME_FORMAT = "%s.corda.net"
|
||||
internal const val DEFAULT = "default"
|
||||
|
||||
internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509ExtendedTrustManager() {
|
||||
companion object {
|
||||
@ -146,7 +149,8 @@ internal fun createClientOpenSslHandler(target: NetworkHostAndPort,
|
||||
return SslHandler(sslEngine)
|
||||
}
|
||||
|
||||
internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory,
|
||||
internal fun createServerSslHelper(keyStore: CertificateStore,
|
||||
keyManagerFactory: KeyManagerFactory,
|
||||
trustManagerFactory: TrustManagerFactory): SslHandler {
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
val keyManagers = keyManagerFactory.keyManagers
|
||||
@ -158,6 +162,9 @@ internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory,
|
||||
sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()
|
||||
sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray()
|
||||
sslEngine.enableSessionCreation = true
|
||||
val sslParameters = sslEngine.sslParameters
|
||||
sslParameters.sniMatchers = listOf(ServerSNIMatcher(keyStore))
|
||||
sslEngine.sslParameters = sslParameters
|
||||
return SslHandler(sslEngine)
|
||||
}
|
||||
|
||||
@ -191,10 +198,55 @@ internal fun createServerOpenSslHandler(keyManagerFactory: KeyManagerFactory,
|
||||
return SslHandler(sslEngine)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a special SNI handler used only when openSSL is used for AMQPServer
|
||||
*/
|
||||
internal fun createServerSNIOpenSslHandler(keyManagerFactoriesMap: Map<String, KeyManagerFactory>,
|
||||
trustManagerFactory: TrustManagerFactory): SniHandler {
|
||||
|
||||
// Default value can be any in the map.
|
||||
val sslCtxBuilder = SslContextBuilder.forServer(keyManagerFactoriesMap.values.first())
|
||||
.sslProvider(SslProvider.OPENSSL)
|
||||
.trustManager(LoggingTrustManagerFactoryWrapper(trustManagerFactory))
|
||||
.clientAuth(ClientAuth.REQUIRE)
|
||||
.ciphers(ArtemisTcpTransport.CIPHER_SUITES)
|
||||
.protocols(*ArtemisTcpTransport.TLS_VERSIONS.toTypedArray())
|
||||
|
||||
val mapping = DomainNameMappingBuilder(sslCtxBuilder.build())
|
||||
|
||||
keyManagerFactoriesMap.forEach {
|
||||
mapping.add(it.key, sslCtxBuilder.keyManager(it.value).build())
|
||||
}
|
||||
|
||||
return SniHandler(mapping.build())
|
||||
}
|
||||
|
||||
internal fun splitKeystore(config: AMQPConfiguration): Map<String, CertHoldingKeyManagerFactoryWrapper> {
|
||||
val keyStore = config.keyStore.value.internal
|
||||
val password = config.keyStore.password.toCharArray()
|
||||
return keyStore.aliases().toList().map { alias ->
|
||||
val key = keyStore.getKey(alias, password)
|
||||
val certs = keyStore.getCertificateChain(alias)
|
||||
val x500Name = keyStore.getCertificate(alias).x509.subjectDN as X500Name
|
||||
val cordaX500Name = CordaX500Name.build(x500Name.asX500Principal())
|
||||
val newKeyStore = KeyStore.getInstance("JKS")
|
||||
newKeyStore.load(null)
|
||||
newKeyStore.setKeyEntry(alias, key, password, certs)
|
||||
val newKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
newKeyManagerFactory.init(newKeyStore, password)
|
||||
x500toHostName(cordaX500Name) to CertHoldingKeyManagerFactoryWrapper(newKeyManagerFactory, config)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
fun KeyManagerFactory.init(keyStore: CertificateStore) = init(keyStore.value.internal, keyStore.password.toCharArray())
|
||||
|
||||
fun TrustManagerFactory.init(trustStore: CertificateStore) = init(trustStore.value.internal)
|
||||
|
||||
/**
|
||||
* Method that converts a [CordaX500Name] to a a valid hostname (RFC-1035). It's used for SNI to indicate the target
|
||||
* when trying to communicate with nodes that reside behind the same firewall. This is a solution to TLS's extension not
|
||||
* yet supporting x500 names as server names
|
||||
*/
|
||||
internal fun x500toHostName(x500Name: CordaX500Name): String {
|
||||
val secureHash = SecureHash.sha256(x500Name.toString())
|
||||
// RFC 1035 specifies a limit 255 bytes for hostnames with each label being 63 bytes or less. Due to this, the string
|
||||
|
@ -0,0 +1,36 @@
|
||||
package net.corda.nodeapi.internal.protonwrapper.netty
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.nodeapi.internal.crypto.x509
|
||||
import sun.security.x509.X500Name
|
||||
import javax.net.ssl.SNIHostName
|
||||
import javax.net.ssl.SNIMatcher
|
||||
import javax.net.ssl.SNIServerName
|
||||
import javax.net.ssl.StandardConstants
|
||||
|
||||
class ServerSNIMatcher(private val keyStore: CertificateStore) : SNIMatcher(0) {
|
||||
|
||||
var matchedAlias: String? = null
|
||||
private set
|
||||
var matchedServerName: String? = null
|
||||
private set
|
||||
|
||||
override fun matches(serverName: SNIServerName): Boolean {
|
||||
if (serverName.type == StandardConstants.SNI_HOST_NAME) {
|
||||
keyStore.aliases().forEach { alias ->
|
||||
val x500Name = keyStore[alias].x509.subjectDN as X500Name
|
||||
val cordaX500Name = CordaX500Name.build(x500Name.asX500Principal())
|
||||
// Convert the CordaX500Name into the expected host name and compare
|
||||
// E.g. O=Corda B, L=London, C=GB becomes 3c6dd991936308edb210555103ffc1bb.corda.net
|
||||
if ((serverName as SNIHostName).asciiName == x500toHostName(cordaX500Name)) {
|
||||
matchedAlias = alias
|
||||
matchedServerName = serverName.asciiName
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
@ -5,7 +5,9 @@ import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.MAX_MESSAGE_SIZE
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import org.junit.Rule
|
||||
@ -41,8 +43,8 @@ class TestKeyManagerFactoryWrapper {
|
||||
config.configureWithDevSSLCertificate()
|
||||
|
||||
val underlyingKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
|
||||
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(underlyingKeyManagerFactory)
|
||||
val amqpConfig = AMQPConfigurationImpl(config.p2pSslOptions.keyStore.get(true), config.p2pSslOptions.trustStore.get(true), MAX_MESSAGE_SIZE)
|
||||
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(underlyingKeyManagerFactory, amqpConfig)
|
||||
wrappedKeyManagerFactory.init(config.p2pSslOptions.keyStore.get())
|
||||
val keyManagers = wrappedKeyManagerFactory.keyManagers
|
||||
assertFalse(keyManagers.isEmpty())
|
||||
@ -74,11 +76,11 @@ class TestKeyManagerFactoryWrapper {
|
||||
config.configureWithDevSSLCertificate()
|
||||
|
||||
val underlyingKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
|
||||
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(underlyingKeyManagerFactory)
|
||||
val amqpConfig = AMQPConfigurationImpl(config.p2pSslOptions.keyStore.get(true), config.p2pSslOptions.trustStore.get(true), MAX_MESSAGE_SIZE)
|
||||
val wrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(underlyingKeyManagerFactory, amqpConfig)
|
||||
wrappedKeyManagerFactory.init(config.p2pSslOptions.keyStore.get())
|
||||
|
||||
val otherWrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(underlyingKeyManagerFactory)
|
||||
val otherWrappedKeyManagerFactory = CertHoldingKeyManagerFactoryWrapper(underlyingKeyManagerFactory, amqpConfig)
|
||||
|
||||
val keyManagers = wrappedKeyManagerFactory.keyManagers
|
||||
assertFalse(keyManagers.isEmpty())
|
||||
@ -92,4 +94,5 @@ class TestKeyManagerFactoryWrapper {
|
||||
assertNull(otherWrappedKeyManagerFactory.getCurrentCertChain())
|
||||
}
|
||||
|
||||
private class AMQPConfigurationImpl(override val keyStore: CertificateStore, override val trustStore: CertificateStore, override val maxMessageSize: Int) : AMQPConfiguration
|
||||
}
|
Reference in New Issue
Block a user