Merge branch 'master' into tudor_merge_os_master

This commit is contained in:
tudor.malene@gmail.com
2018-10-02 15:50:20 +01:00
510 changed files with 10259 additions and 5920 deletions

View File

@ -0,0 +1,5 @@
package net.corda.nodeapi
import java.nio.file.Path
data class BrokerRpcSslOptions(val keyStorePath: Path, val keyStorePassword: String)

View File

@ -3,9 +3,8 @@ package net.corda.nodeapi.internal
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
import org.apache.activemq.artemis.api.core.client.ClientProducer
@ -18,7 +17,7 @@ interface ArtemisSessionProvider {
val started: ArtemisMessagingClient.Started?
}
class ArtemisMessagingClient(private val config: SSLConfiguration,
class ArtemisMessagingClient(private val config: MutualSslConfiguration,
private val serverAddress: NetworkHostAndPort,
private val maxMessageSize: Int) : ArtemisSessionProvider {
companion object {
@ -59,8 +58,11 @@ class ArtemisMessagingClient(private val config: SSLConfiguration,
override fun stop() = synchronized(this) {
started?.run {
producer.close()
// Ensure any trailing messages are committed to the journal
session.commit()
// Since we are leaking the session outside of this class it may well be already closed.
if(!session.isClosed) {
// Ensure any trailing messages are committed to the journal
session.commit()
}
// Closing the factory closes all the sessions it produced as well.
sessionFactory.close()
}

View File

@ -6,7 +6,6 @@ import net.corda.core.messaging.MessageRecipientGroup
import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.NetworkHostAndPort
import org.apache.activemq.artemis.api.core.Message
import org.apache.activemq.artemis.api.core.SimpleString
import java.security.PublicKey
@ -77,9 +76,7 @@ class ArtemisMessagingComponent {
val queueName: String
}
interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient {
val hostAndPort: NetworkHostAndPort
}
interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient
/**
* This is the class used to implement [SingleMessageRecipient], for now. Note that in future this class
@ -90,12 +87,11 @@ class ArtemisMessagingComponent {
* an advertised service's queue.
*
* @param queueName The name of the queue this address is associated with.
* @param hostAndPort The address of the node.
*/
@CordaSerializable
data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress {
constructor(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort) :
this("$PEERS_PREFIX${peerIdentity.toStringShort()}", hostAndPort)
data class NodeAddress(override val queueName: String) : ArtemisPeerAddress {
constructor(peerIdentity: PublicKey) :
this("$PEERS_PREFIX${peerIdentity.toStringShort()}")
}
/**

View File

@ -1,44 +1,29 @@
package net.corda.nodeapi
package net.corda.nodeapi.internal
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.requireOnDefaultFileSystem
import net.corda.nodeapi.BrokerRpcSslOptions
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
import net.corda.nodeapi.internal.config.SslConfiguration
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import org.apache.activemq.artemis.api.core.TransportConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
import java.nio.file.Path
/** Class to set Artemis TCP configuration options. */
// This avoids internal types from leaking in the public API. The "external" ArtemisTcpTransport delegates to this internal one.
class ArtemisTcpTransport {
companion object {
/**
* Corda supported TLS schemes.
* <p><ul>
* <li>TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
* <li>TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* <li>TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
* </ul></p>
* As shown above, current version restricts enabled TLS cipher suites to:
* AES128 using Galois/Counter Mode (GCM) for the block cipher being used to encrypt the message stream.
* SHA256 as message authentication algorithm.
* Ephemeral Diffie Hellman key exchange for advanced forward secrecy. ECDHE is preferred, but DHE is also
* supported in case one wants to completely avoid the use of ECC for TLS.
* ECDSA and RSA for digital signatures. Our self-generated certificates all use ECDSA for handshakes,
* but we allow classical RSA certificates to work in case one uses external tools or cloud providers or HSMs
* that do not support ECC certificates.
*/
val CIPHER_SUITES = listOf(
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
)
/** Supported TLS versions, currently TLSv1.2 only. */
val TLS_VERSIONS = listOf("TLSv1.2")
private fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf(
internal fun defaultArtemisOptions(hostAndPort: NetworkHostAndPort) = mapOf(
// Basic TCP target details.
TransportConstants.HOST_PROP_NAME to hostAndPort.host,
TransportConstants.PORT_PROP_NAME to hostAndPort.port,
@ -54,18 +39,47 @@ class ArtemisTcpTransport {
//hick-ups under high load (CORDA-1336)
TransportConstants.DIRECT_DELIVER to false)
private val defaultSSLOptions = mapOf(
internal val defaultSSLOptions = mapOf(
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to TLS_VERSIONS.joinToString(","))
private fun SSLConfiguration.toTransportOptions() = mapOf(
private fun SslConfiguration.toTransportOptions(): Map<String, Any> {
val options = mutableMapOf<String, Any>()
(keyStore to trustStore).addToTransportOptions(options)
return options
}
private fun Pair<FileBasedCertificateStoreSupplier?, FileBasedCertificateStoreSupplier?>.addToTransportOptions(options: MutableMap<String, Any>) {
val keyStore = first
val trustStore = second
keyStore?.let {
with (it) {
path.requireOnDefaultFileSystem()
options.putAll(get().toKeyStoreTransportOptions(path))
}
}
trustStore?.let {
with (it) {
path.requireOnDefaultFileSystem()
options.putAll(get().toTrustStoreTransportOptions(path))
}
}
}
private fun CertificateStore.toKeyStoreTransportOptions(path: Path) = mapOf(
TransportConstants.SSL_ENABLED_PROP_NAME to true,
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS",
TransportConstants.KEYSTORE_PATH_PROP_NAME to sslKeystore,
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword,
TransportConstants.KEYSTORE_PATH_PROP_NAME to path,
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to password,
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true)
private fun CertificateStore.toTrustStoreTransportOptions(path: Path) = mapOf(
TransportConstants.SSL_ENABLED_PROP_NAME to true,
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to trustStoreFile,
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to trustStorePassword,
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to path,
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to password,
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true)
private fun ClientRpcSslOptions.toTransportOptions() = mapOf(
@ -81,34 +95,39 @@ class ArtemisTcpTransport {
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to keyStorePassword,
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to false)
private val acceptorFactoryClassName = "org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory"
private val connectorFactoryClassName = NettyConnectorFactory::class.java.name
internal val acceptorFactoryClassName = "org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory"
internal val connectorFactoryClassName = NettyConnectorFactory::class.java.name
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration {
return p2pAcceptorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL)
}
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: MutualSslConfiguration?, enableSSL: Boolean = true): TransportConfiguration {
return p2pConnectorTcpTransport(hostAndPort, config?.keyStore, config?.trustStore, enableSSL = enableSSL)
}
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration {
fun p2pAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
if (config != null && enableSSL) {
config.sslKeystore.requireOnDefaultFileSystem()
config.trustStoreFile.requireOnDefaultFileSystem()
if (enableSSL) {
options.putAll(defaultSSLOptions)
options.putAll(config.toTransportOptions())
(keyStore to trustStore).addToTransportOptions(options)
}
return TransportConfiguration(acceptorFactoryClassName, options)
}
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
fun p2pConnectorTcpTransport(hostAndPort: NetworkHostAndPort, keyStore: FileBasedCertificateStoreSupplier?, trustStore: FileBasedCertificateStoreSupplier?, enableSSL: Boolean = true): TransportConfiguration {
if (config != null && enableSSL) {
config.sslKeystore.requireOnDefaultFileSystem()
config.trustStoreFile.requireOnDefaultFileSystem()
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
if (enableSSL) {
options.putAll(defaultSSLOptions)
options.putAll(config.toTransportOptions())
(keyStore to trustStore).addToTransportOptions(options)
}
return TransportConfiguration(connectorFactoryClassName, options)
}
/** [TransportConfiguration] for RPC TCP communication - server side. */
fun rpcAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: BrokerRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
@ -120,8 +139,6 @@ class ArtemisTcpTransport {
return TransportConfiguration(acceptorFactoryClassName, options)
}
/** [TransportConfiguration] for RPC TCP communication
* This is the Transport that connects the client JVM to the broker. */
fun rpcConnectorTcpTransport(hostAndPort: NetworkHostAndPort, config: ClientRpcSslOptions?, enableSSL: Boolean = true): TransportConfiguration {
val options = defaultArtemisOptions(hostAndPort).toMutableMap()
@ -133,19 +150,16 @@ class ArtemisTcpTransport {
return TransportConfiguration(connectorFactoryClassName, options)
}
/** Create as list of [TransportConfiguration]. **/
fun rpcConnectorTcpTransportsFromList(hostAndPortList: List<NetworkHostAndPort>, config: ClientRpcSslOptions?, enableSSL: Boolean = true): List<TransportConfiguration> = hostAndPortList.map {
rpcConnectorTcpTransport(it, config, enableSSL)
}
fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration {
fun rpcInternalClientTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration {
return TransportConfiguration(connectorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions())
}
fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SSLConfiguration): TransportConfiguration {
fun rpcInternalAcceptorTcpTransport(hostAndPort: NetworkHostAndPort, config: SslConfiguration): TransportConfiguration {
return TransportConfiguration(acceptorFactoryClassName, defaultArtemisOptions(hostAndPort) + defaultSSLOptions + config.toTransportOptions())
}
}
}
data class BrokerRpcSslOptions(val keyStorePath: Path, val keyStorePassword: String)
}

View File

@ -7,10 +7,13 @@ import net.corda.core.identity.Party
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.utilities.trace
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
import net.corda.nodeapi.internal.config.SslConfiguration
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX
import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.security.KeyPair
@ -24,22 +27,17 @@ import java.security.PublicKey
object DevIdentityGenerator {
private val log = LoggerFactory.getLogger(javaClass)
// TODO These don't need to be prefixes but can be the full aliases
// TODO Move these constants out of here as the node needs access to them
const val NODE_IDENTITY_ALIAS_PREFIX = "identity"
const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary"
/** Install a node key store for the given node directory using the given legal name. */
fun installKeyStoreWithNodeIdentity(nodeDir: Path, legalName: CordaX500Name): Party {
val nodeSslConfig = object : NodeSSLConfiguration {
override val baseDirectory = nodeDir
override val keyStorePassword: String = "cordacadevpass"
override val trustStorePassword get() = throw NotImplementedError("Not expected to be called")
override val crlCheckSoftFail: Boolean = true
}
val certificatesDirectory = nodeDir / "certificates"
val signingCertStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "nodekeystore.jks", "cordacadevpass")
val p2pKeyStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "sslkeystore.jks", "cordacadevpass")
val p2pTrustStore = FileBasedCertificateStoreSupplier(certificatesDirectory / "truststore.jks", "trustpass")
val p2pSslConfig = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore)
nodeSslConfig.certificatesDirectory.createDirectories()
val (nodeKeyStore) = nodeSslConfig.createDevKeyStores(legalName)
certificatesDirectory.createDirectories()
val nodeKeyStore = signingCertStore.get(true).also { it.registerDevSigningCertificates(legalName) }
p2pSslConfig.keyStore.get(true).also { it.registerDevP2pCertificates(legalName) }
val identity = nodeKeyStore.storeLegalIdentity("$NODE_IDENTITY_ALIAS_PREFIX-private-key")
return identity.party

View File

@ -5,7 +5,7 @@ import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.toX500Name
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.*
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
@ -20,48 +20,43 @@ import javax.security.auth.x500.X500Principal
* Create the node and SSL key stores needed by a node. The node key store will be populated with a node CA cert (using
* the given legal name), and the SSL key store will store the TLS cert which is a sub-cert of the node CA.
*/
fun SSLConfiguration.createDevKeyStores(legalName: CordaX500Name,
rootCert: X509Certificate = DEV_ROOT_CA.certificate,
intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA): Pair<X509KeyStore, X509KeyStore> {
val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName)
val nodeKeyStore = loadNodeKeyStore(createNew = true)
nodeKeyStore.update {
setPrivateKey(
X509Utilities.CORDA_CLIENT_CA,
nodeCaKeyPair.private,
listOf(nodeCaCert, intermediateCa.certificate, rootCert))
fun CertificateStore.registerDevSigningCertificates(legalName: CordaX500Name,
rootCert: X509Certificate = DEV_ROOT_CA.certificate,
intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA,
devNodeCa: CertificateAndKeyPair = createDevNodeCa(intermediateCa, legalName)) {
update {
setPrivateKey(X509Utilities.CORDA_CLIENT_CA, devNodeCa.keyPair.private, listOf(devNodeCa.certificate, intermediateCa.certificate, rootCert))
}
val sslKeyStore = loadSslKeyStore(createNew = true)
sslKeyStore.update {
val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName.x500Principal, tlsKeyPair.public)
setPrivateKey(
X509Utilities.CORDA_CLIENT_TLS,
tlsKeyPair.private,
listOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCert))
}
return Pair(nodeKeyStore, sslKeyStore)
}
fun X509KeyStore.storeLegalIdentity(alias: String, keyPair: KeyPair = Crypto.generateKeyPair()): PartyAndCertificate {
val nodeCaCertPath = getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
// Assume key password = store password.
val nodeCaCertAndKeyPair = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
// Create new keys and store in keystore.
val identityCert = X509Utilities.createCertificate(
CertificateType.LEGAL_IDENTITY,
nodeCaCertAndKeyPair.certificate,
nodeCaCertAndKeyPair.keyPair,
nodeCaCertAndKeyPair.certificate.subjectX500Principal,
keyPair.public)
// TODO: X509Utilities.validateCertificateChain()
// Assume key password = store password.
val identityCertPath = listOf(identityCert) + nodeCaCertPath
setPrivateKey(alias, keyPair.private, identityCertPath)
save()
fun CertificateStore.registerDevP2pCertificates(legalName: CordaX500Name,
rootCert: X509Certificate = DEV_ROOT_CA.certificate,
intermediateCa: CertificateAndKeyPair = DEV_INTERMEDIATE_CA,
devNodeCa: CertificateAndKeyPair = createDevNodeCa(intermediateCa, legalName)) {
update {
val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, devNodeCa.certificate, devNodeCa.keyPair, legalName.x500Principal, tlsKeyPair.public)
setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(tlsCert, devNodeCa.certificate, intermediateCa.certificate, rootCert))
}
}
fun CertificateStore.storeLegalIdentity(alias: String, keyPair: KeyPair = Crypto.generateKeyPair()): PartyAndCertificate {
val identityCertPath = query {
val nodeCaCertPath = getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
// Assume key password = store password.
val nodeCaCertAndKeyPair = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
// Create new keys and store in keystore.
val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, nodeCaCertAndKeyPair.certificate, nodeCaCertAndKeyPair.keyPair, nodeCaCertAndKeyPair.certificate.subjectX500Principal, keyPair.public)
// TODO: X509Utilities.validateCertificateChain()
// Assume key password = store password.
listOf(identityCert) + nodeCaCertPath
}
update {
setPrivateKey(alias, keyPair.private, identityCertPath)
}
return PartyAndCertificate(X509Utilities.buildCertPath(identityCertPath))
}
@ -105,8 +100,10 @@ const val DEV_CA_TRUST_STORE_PASS: String = "trustpass"
// We need a class so that we can get hold of the class loader
internal object DevCaHelper {
fun loadDevCa(alias: String): CertificateAndKeyPair {
// TODO: Should be identity scheme
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_KEY_STORE_FILE"), DEV_CA_KEY_STORE_PASS)
return caKeyStore.getCertificateAndKeyPair(alias, DEV_CA_PRIVATE_KEY_PASS)
return loadDevCaKeyStore().query { getCertificateAndKeyPair(alias, DEV_CA_PRIVATE_KEY_PASS) }
}
}
fun loadDevCaKeyStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource("certificates/$DEV_CA_KEY_STORE_FILE", DEV_CA_KEY_STORE_PASS, classLoader)
fun loadDevCaTrustStore(classLoader: ClassLoader = DevCaHelper::class.java.classLoader): CertificateStore = CertificateStore.fromResource("certificates/$DEV_CA_TRUST_STORE_FILE", DEV_CA_TRUST_STORE_PASS, classLoader)

View File

@ -1,4 +1,5 @@
package net.corda.nodeapi.internal
// TODO: Add to Corda node.conf to allow customisation
const val NODE_INFO_DIRECTORY = "additional-node-infos"
const val NODE_INFO_DIRECTORY = "additional-node-infos"
const val PLATFORM_VERSION = 4

View File

@ -0,0 +1,52 @@
package net.corda.nodeapi.internal
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.StateMachineUpdate
import rx.Observable
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import java.util.concurrent.TimeUnit
/**
* Returns a [DataFeed] of the number of pending flows. The [Observable] for the updates will complete the moment all pending flows will have terminated.
*/
fun CordaRPCOps.pendingFlowsCount(): DataFeed<Int, Pair<Int, Int>> {
val updates = PublishSubject.create<Pair<Int, Int>>()
val initialPendingFlowsCount = stateMachinesFeed().let {
var completedFlowsCount = 0
var pendingFlowsCount = it.snapshot.size
it.updates.observeOn(Schedulers.io()).subscribe({ update ->
when (update) {
is StateMachineUpdate.Added -> {
pendingFlowsCount++
updates.onNext(completedFlowsCount to pendingFlowsCount)
}
is StateMachineUpdate.Removed -> {
completedFlowsCount++
updates.onNext(completedFlowsCount to pendingFlowsCount)
if (completedFlowsCount == pendingFlowsCount) {
updates.onCompleted()
}
}
}
}, updates::onError)
if (pendingFlowsCount == 0) {
updates.onCompleted()
}
pendingFlowsCount
}
return DataFeed(initialPendingFlowsCount, updates)
}
/**
* Returns an [Observable] that will complete when the node will have cancelled the draining shutdown hook.
*
* @param interval the value of the polling interval, default is 5.
* @param unit the time unit of the polling interval, default is [TimeUnit.SECONDS].
*/
fun CordaRPCOps.hasCancelledDrainingShutdown(interval: Long = 5, unit: TimeUnit = TimeUnit.SECONDS): Observable<Unit> {
return Observable.interval(interval, unit).map { isWaitingForShutdown() }.takeFirst { waiting -> waiting == false }.map { Unit }
}

View File

@ -4,17 +4,15 @@ import io.netty.channel.EventLoopGroup
import io.netty.channel.nio.NioEventLoopGroup
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.VisibleForTesting
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.ArtemisMessagingClient
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_P2P_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2PMessagingHeaders
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
import net.corda.nodeapi.internal.ArtemisSessionProvider
import net.corda.nodeapi.internal.bridging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
import net.corda.nodeapi.internal.protonwrapper.netty.AMQPConfiguration
@ -25,7 +23,6 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.slf4j.MDC
import rx.Subscription
import java.security.KeyStore
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@ -37,26 +34,24 @@ import kotlin.concurrent.withLock
* The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager.
*/
@VisibleForTesting
class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, private val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager {
class AMQPBridgeManager(config: MutualSslConfiguration, maxMessageSize: Int,
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
private val bridgeMetricsService: BridgeMetricsService? = null) : BridgeManager {
private val lock = ReentrantLock()
private val bridgeNameToBridgeMap = mutableMapOf<String, AMQPBridge>()
private val queueNamesToBridgesMap = mutableMapOf<String, MutableList<AMQPBridge>>()
private class AMQPConfigurationImpl private constructor(override val keyStore: KeyStore,
override val keyStorePrivateKeyPassword: CharArray,
override val trustStore: KeyStore,
private class AMQPConfigurationImpl private constructor(override val keyStore: CertificateStore,
override val trustStore: CertificateStore,
override val maxMessageSize: Int) : AMQPConfiguration {
constructor(config: NodeSSLConfiguration, maxMessageSize: Int) : this(config.loadSslKeyStore().internal,
config.keyStorePassword.toCharArray(),
config.loadTrustStore().internal,
maxMessageSize)
constructor(config: MutualSslConfiguration, maxMessageSize: Int) : this(config.keyStore.get(), config.trustStore.get(), maxMessageSize)
}
private val amqpConfig: AMQPConfiguration = AMQPConfigurationImpl(config, maxMessageSize)
private var sharedEventLoopGroup: EventLoopGroup? = null
private var artemis: ArtemisSessionProvider? = null
constructor(config: NodeSSLConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) })
constructor(config: MutualSslConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) })
companion object {
private const val NUM_BRIDGE_THREADS = 0 // Default sized pool
@ -70,14 +65,14 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva
* 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(private val queueName: String,
private val target: NetworkHostAndPort,
private val legalNames: Set<CordaX500Name>,
private class AMQPBridge(val queueName: String,
val targets: List<NetworkHostAndPort>,
val legalNames: Set<CordaX500Name>,
private val amqpConfig: AMQPConfiguration,
sharedEventGroup: EventLoopGroup,
private val artemis: ArtemisSessionProvider) {
private val artemis: ArtemisSessionProvider,
private val bridgeMetricsService: BridgeMetricsService?) {
companion object {
fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort"
private val log = contextLogger()
}
@ -85,8 +80,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva
val oldMDC = MDC.getCopyOfContextMap()
try {
MDC.put("queueName", queueName)
MDC.put("target", target.toString())
MDC.put("bridgeName", bridgeName)
MDC.put("targets", targets.joinToString(separator = ";") { it.toString() })
MDC.put("legalNames", legalNames.joinToString(separator = ";") { it.toString() })
MDC.put("maxMessageSize", amqpConfig.maxMessageSize.toString())
block()
@ -105,8 +99,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva
private fun logWarnWithMDC(msg: String) = withMDC { log.warn(msg) }
val amqpClient = AMQPClient(listOf(target), legalNames, amqpConfig, sharedThreadPool = sharedEventGroup)
val bridgeName: String get() = getBridgeName(queueName, target)
val amqpClient = AMQPClient(targets, legalNames, amqpConfig, sharedThreadPool = sharedEventGroup)
private val lock = ReentrantLock() // lock to serialise session level access
private var session: ClientSession? = null
private var consumer: ClientConsumer? = null
@ -114,7 +107,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva
fun start() {
logInfoWithMDC("Create new AMQP bridge")
connectedSubscription = amqpClient.onConnection.subscribe({ x -> onSocketConnected(x.connected) })
connectedSubscription = amqpClient.onConnection.subscribe { x -> onSocketConnected(x.connected) }
amqpClient.start()
}
@ -138,6 +131,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva
synchronized(artemis) {
if (connected) {
logInfoWithMDC("Bridge Connected")
bridgeMetricsService?.bridgeConnected(targets, legalNames)
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
@ -147,6 +141,7 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva
session.start()
} else {
logInfoWithMDC("Bridge Disconnected")
bridgeMetricsService?.bridgeDisconnected(targets, legalNames)
consumer?.close()
consumer = null
session?.stop()
@ -158,8 +153,10 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva
private fun clientArtemisMessageHandler(artemisMessage: ClientMessage) {
if (artemisMessage.bodySize > amqpConfig.maxMessageSize) {
logWarnWithMDC("Message exceeds maxMessageSize network parameter, maxMessageSize: [${amqpConfig.maxMessageSize}], message size: [${artemisMessage.bodySize}], " +
"dropping message, uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}")
val msg = "Message exceeds maxMessageSize network parameter, maxMessageSize: [${amqpConfig.maxMessageSize}], message size: [${artemisMessage.bodySize}], " +
"dropping message, uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}"
logWarnWithMDC(msg)
bridgeMetricsService?.packetDropEvent(artemisMessage, msg)
// Ack the message to prevent same message being sent to us again.
artemisMessage.acknowledge()
return
@ -195,42 +192,43 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva
}
}
amqpClient.write(sendableMessage)
bridgeMetricsService?.packetAcceptedEvent(sendableMessage)
}
}
private fun gatherAddresses(node: NodeInfo): List<ArtemisMessagingComponent.NodeAddress> {
return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, node.addresses[0]) }
}
override fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set<CordaX500Name>) {
if (bridgeExists(getBridgeName(queueName, target))) {
return
}
val newBridge = AMQPBridge(queueName, target, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!)
lock.withLock {
bridgeNameToBridgeMap[newBridge.bridgeName] = newBridge
override fun deployBridge(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) }) {
return
}
}
val newBridge = AMQPBridge(queueName, targets, legalNames, amqpConfig, sharedEventLoopGroup!!, artemis!!, bridgeMetricsService)
bridges += newBridge
bridgeMetricsService?.bridgeCreated(targets, legalNames)
newBridge
}
newBridge.start()
}
override fun destroyBridges(node: NodeInfo) {
override fun destroyBridge(queueName: String, targets: List<NetworkHostAndPort>) {
lock.withLock {
gatherAddresses(node).forEach {
val bridge = bridgeNameToBridgeMap.remove(getBridgeName(it.queueName, it.hostAndPort))
bridge?.stop()
val bridges = queueNamesToBridgesMap[queueName] ?: mutableListOf()
for (target in targets) {
val bridge = bridges.firstOrNull { it.targets.contains(target) }
if (bridge != null) {
bridges -= bridge
if (bridges.isEmpty()) {
queueNamesToBridgesMap.remove(queueName)
}
bridge.stop()
bridgeMetricsService?.bridgeDestroyed(bridge.targets, bridge.legalNames)
}
}
}
}
override fun destroyBridge(queueName: String, hostAndPort: NetworkHostAndPort) {
lock.withLock {
val bridge = bridgeNameToBridgeMap.remove(getBridgeName(queueName, hostAndPort))
bridge?.stop()
}
}
override fun bridgeExists(bridgeName: String): Boolean = lock.withLock { bridgeNameToBridgeMap.containsKey(bridgeName) }
override fun start() {
sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS)
val artemis = artemisMessageClientFactory()
@ -242,13 +240,13 @@ class AMQPBridgeManager(config: NodeSSLConfiguration, maxMessageSize: Int, priva
override fun close() {
lock.withLock {
for (bridge in bridgeNameToBridgeMap.values) {
for (bridge in queueNamesToBridgesMap.values.flatten()) {
bridge.stop()
}
sharedEventLoopGroup?.shutdownGracefully()
sharedEventLoopGroup?.terminationFuture()?.sync()
sharedEventLoopGroup = null
bridgeNameToBridgeMap.clear()
queueNamesToBridgesMap.clear()
artemis?.stop()
}
}

View File

@ -11,23 +11,25 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOT
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.ArtemisSessionProvider
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ClientConsumer
import org.apache.activemq.artemis.api.core.client.ClientMessage
import java.util.*
class BridgeControlListener(val config: NodeSSLConfiguration,
class BridgeControlListener(val config: MutualSslConfiguration,
maxMessageSize: Int,
val artemisMessageClientFactory: () -> ArtemisSessionProvider) : AutoCloseable {
private val artemisMessageClientFactory: () -> ArtemisSessionProvider,
bridgeMetricsService: BridgeMetricsService? = null) : AutoCloseable {
private val bridgeId: String = UUID.randomUUID().toString()
private val bridgeManager: BridgeManager = AMQPBridgeManager(config, maxMessageSize, artemisMessageClientFactory)
private val bridgeManager: BridgeManager = AMQPBridgeManager(config, maxMessageSize,
artemisMessageClientFactory, bridgeMetricsService)
private val validInboundQueues = mutableSetOf<String>()
private var artemis: ArtemisSessionProvider? = null
private var controlConsumer: ClientConsumer? = null
constructor(config: NodeSSLConfiguration,
constructor(config: MutualSslConfiguration,
p2pAddress: NetworkHostAndPort,
maxMessageSize: Int) : this(config, maxMessageSize, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) })
@ -98,7 +100,7 @@ class BridgeControlListener(val config: NodeSSLConfiguration,
return
}
for (outQueue in controlMessage.sendQueues) {
bridgeManager.deployBridge(outQueue.queueName, outQueue.targets.first(), outQueue.legalNames.toSet())
bridgeManager.deployBridge(outQueue.queueName, outQueue.targets, outQueue.legalNames.toSet())
}
validInboundQueues.addAll(controlMessage.inboxQueues)
}
@ -110,14 +112,14 @@ class BridgeControlListener(val config: NodeSSLConfiguration,
log.error("Invalid queue names in control message $controlMessage")
return
}
bridgeManager.deployBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets.first(), controlMessage.bridgeInfo.legalNames.toSet())
bridgeManager.deployBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets, controlMessage.bridgeInfo.legalNames.toSet())
}
is BridgeControl.Delete -> {
if (!controlMessage.bridgeInfo.queueName.startsWith(PEERS_PREFIX)) {
log.error("Invalid queue names in control message $controlMessage")
return
}
bridgeManager.destroyBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets.first())
bridgeManager.destroyBridge(controlMessage.bridgeInfo.queueName, controlMessage.bridgeInfo.targets)
}
}
}

View File

@ -2,7 +2,6 @@ package net.corda.nodeapi.internal.bridging
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.VisibleForTesting
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort
/**
@ -10,13 +9,9 @@ import net.corda.core.utilities.NetworkHostAndPort
*/
@VisibleForTesting
interface BridgeManager : AutoCloseable {
fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set<CordaX500Name>)
fun deployBridge(queueName: String, targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
fun destroyBridges(node: NodeInfo)
fun destroyBridge(queueName: String, hostAndPort: NetworkHostAndPort)
fun bridgeExists(bridgeName: String): Boolean
fun destroyBridge(queueName: String, targets: List<NetworkHostAndPort>)
fun start()

View File

@ -0,0 +1,15 @@
package net.corda.nodeapi.internal.bridging
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.protonwrapper.messages.SendableMessage
import org.apache.activemq.artemis.api.core.client.ClientMessage
interface BridgeMetricsService {
fun bridgeCreated(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
fun bridgeConnected(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
fun packetDropEvent(artemisMessage: ClientMessage, msg: String)
fun packetAcceptedEvent(sendableMessage: SendableMessage)
fun bridgeDisconnected(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
fun bridgeDestroyed(targets: List<NetworkHostAndPort>, legalNames: Set<CordaX500Name>)
}

View File

@ -0,0 +1,82 @@
package net.corda.nodeapi.internal.config
import net.corda.core.internal.outputStream
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.addOrReplaceCertificate
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.OpenOption
import java.nio.file.Path
import java.security.cert.X509Certificate
interface CertificateStore : Iterable<Pair<String, X509Certificate>> {
companion object {
fun of(store: X509KeyStore, password: String): CertificateStore = DelegatingCertificateStore(store, password)
fun fromFile(storePath: Path, password: String, createNew: Boolean): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromFile(storePath, password, createNew), password)
fun fromInputStream(stream: InputStream, password: String): CertificateStore = DelegatingCertificateStore(X509KeyStore.fromInputStream(stream, password), password)
fun fromResource(storeResourceName: String, password: String, classLoader: ClassLoader = Thread.currentThread().contextClassLoader): CertificateStore = fromInputStream(classLoader.getResourceAsStream(storeResourceName), password)
}
val value: X509KeyStore
val password: String
fun writeTo(stream: OutputStream) = value.internal.store(stream, password.toCharArray())
fun writeTo(path: Path, vararg options: OpenOption) = path.outputStream(*options)
fun update(action: X509KeyStore.() -> Unit) {
val result = action.invoke(value)
value.save()
return result
}
fun <RESULT> query(action: X509KeyStore.() -> RESULT): RESULT {
return action.invoke(value)
}
operator fun set(alias: String, certificate: X509Certificate) {
update {
internal.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, certificate)
}
}
override fun iterator(): Iterator<Pair<String, X509Certificate>> {
return query {
aliases()
}.asSequence().map { alias -> alias to get(alias) }.iterator()
}
fun forEach(action: (alias: String, certificate: X509Certificate) -> Unit) {
forEach { (alias, certificate) -> action.invoke(alias, certificate) }
}
/**
* @throws IllegalArgumentException if no certificate for the alias is found, or if the certificate is not an [X509Certificate].
*/
operator fun get(alias: String): X509Certificate {
return query {
getCertificate(alias)
}
}
operator fun contains(alias: String): Boolean = value.contains(alias)
fun copyTo(certificateStore: CertificateStore) {
certificateStore.update {
this@CertificateStore.forEach(::setCertificate)
}
}
}
private class DelegatingCertificateStore(override val value: X509KeyStore, override val password: String) : CertificateStore

View File

@ -0,0 +1,24 @@
package net.corda.nodeapi.internal.config
import java.io.IOException
import java.nio.file.Path
interface CertificateStoreSupplier {
fun get(createNew: Boolean = false): CertificateStore
fun getOptional(): CertificateStore? {
return try {
get()
} catch (e: IOException) {
null
}
}
}
// TODO replace reference to FileBasedCertificateStoreSupplier with CertificateStoreSupplier, after coming up with a way of passing certificate stores to Artemis.
class FileBasedCertificateStoreSupplier(val path: Path, val password: String) : CertificateStoreSupplier {
override fun get(createNew: Boolean) = CertificateStore.fromFile(path, password, createNew)
}

View File

@ -1,33 +0,0 @@
package net.corda.nodeapi.internal.config
import net.corda.core.internal.div
import net.corda.nodeapi.internal.crypto.X509KeyStore
import java.nio.file.Path
interface SSLConfiguration {
val keyStorePassword: String
val trustStorePassword: String
val certificatesDirectory: Path
val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks"
// TODO This looks like it should be in NodeSSLConfiguration
val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks"
val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks"
val crlCheckSoftFail: Boolean
fun loadTrustStore(createNew: Boolean = false): X509KeyStore {
return X509KeyStore.fromFile(trustStoreFile, trustStorePassword, createNew)
}
fun loadNodeKeyStore(createNew: Boolean = false): X509KeyStore {
return X509KeyStore.fromFile(nodeKeystore, keyStorePassword, createNew)
}
fun loadSslKeyStore(createNew: Boolean = false): X509KeyStore {
return X509KeyStore.fromFile(sslKeystore, keyStorePassword, createNew)
}
}
interface NodeSSLConfiguration : SSLConfiguration {
val baseDirectory: Path
override val certificatesDirectory: Path get() = baseDirectory / "certificates"
}

View File

@ -0,0 +1,23 @@
package net.corda.nodeapi.internal.config
interface SslConfiguration {
val keyStore: FileBasedCertificateStoreSupplier?
val trustStore: FileBasedCertificateStoreSupplier?
companion object {
fun mutual(keyStore: FileBasedCertificateStoreSupplier, trustStore: FileBasedCertificateStoreSupplier): MutualSslConfiguration {
return MutualSslOptions(keyStore, trustStore)
}
}
}
interface MutualSslConfiguration : SslConfiguration {
override val keyStore: FileBasedCertificateStoreSupplier
override val trustStore: FileBasedCertificateStoreSupplier
}
private class MutualSslOptions(override val keyStore: FileBasedCertificateStoreSupplier, override val trustStore: FileBasedCertificateStoreSupplier) : MutualSslConfiguration

View File

@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.Crypto
import net.corda.core.internal.uncheckedCast
import java.io.InputStream
import java.nio.file.Path
import java.security.KeyPair
import java.security.KeyStore
@ -30,6 +31,14 @@ class X509KeyStore private constructor(val internal: KeyStore, private val store
val internal: KeyStore = if (createNew) loadOrCreateKeyStore(keyStoreFile, storePassword) else loadKeyStore(keyStoreFile, storePassword)
return X509KeyStore(internal, storePassword, keyStoreFile)
}
/**
* Reads a [KeyStore] from an [InputStream].
*/
fun fromInputStream(stream: InputStream, storePassword: String): X509KeyStore {
val internal = loadKeyStore(stream, storePassword)
return X509KeyStore(internal, storePassword)
}
}
operator fun contains(alias: String): Boolean = internal.containsAlias(alias)

View File

@ -41,13 +41,23 @@ object X509Utilities {
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512
val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
// TODO This class is more of a general purpose utility class and as such these constants belong elsewhere
// TODO This class is more of a general purpose utility class and as such these constants belong elsewhere.
// Aliases for private keys and certificates.
const val CORDA_ROOT_CA = "cordarootca"
const val CORDA_INTERMEDIATE_CA = "cordaintermediateca"
const val CORDA_CLIENT_TLS = "cordaclienttls"
const val CORDA_CLIENT_CA = "cordaclientca"
// TODO These don't need to be prefixes, but can be the full aliases. However, because they are used as key aliases
// we should ensure that:
// a) they always contain valid characters, preferably [A-Za-z0-9] in order to be supported by the majority of
// crypto service implementations (i.e., HSMs).
// b) they are at most 127 chars in length (i.e., as of 2018, Azure Key Vault does not support bigger aliases).
const val NODE_IDENTITY_ALIAS_PREFIX = "identity"
// TODO Hyphen (-) seems to be supported by the major HSM vendors, but we should consider remove it in the
// future and stick to [A-Za-z0-9].
const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary"
val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days)
/**
@ -402,7 +412,7 @@ enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurpo
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = true,
role = CertRole.INTERMEDIATE_CA
role = CertRole.DOORMAN_CA
),
NETWORK_MAP(

View File

@ -147,11 +147,19 @@ internal constructor(private val initSerEnv: Boolean,
}
}
/** Entry point for Cordform */
/** Old Entry point for Cordform
*
* TODO: Remove once the gradle plugins are updated to 4.0.30
*/
fun bootstrap(directory: Path, cordappJars: List<Path>) {
bootstrap(directory, cordappJars, copyCordapps = true, fromCordform = true)
}
/** Entry point for Cordform */
fun bootstrapCordform(directory: Path, cordappJars: List<Path>) {
bootstrap(directory, cordappJars, copyCordapps = false, fromCordform = true)
}
/** Entry point for the tool */
fun bootstrap(directory: Path, copyCordapps: Boolean) {
// Don't accidently include the bootstrapper jar as a CorDapp!

View File

@ -2,13 +2,10 @@ package net.corda.nodeapi.internal.network
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.CertRole
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.internal.signWithCert
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.nodeapi.internal.crypto.X509Utilities
import java.security.cert.X509Certificate
import java.time.Instant
@ -57,20 +54,14 @@ data class ParametersUpdate(
val updateDeadline: Instant
)
/** Verify that a Network Map certificate is issued by Root CA and its [CertRole] is correct. */
// TODO: Current implementation works under the assumption that there are no intermediate CAs between Root and
// Network Map. Consider a more flexible implementation without the above assumption.
/** Verify that a Network Map certificate path and its [CertRole] is correct. */
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T {
require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert)
val path = if (sig.parentCertsChain.isEmpty()) {
listOf(sig.by, rootCert)
} else {
sig.fullCertChain
}
X509Utilities.validateCertificateChain(rootCert, path)
return verified()
}
class NetworkMapAndSigned private constructor(val networkMap: NetworkMap, val signed: SignedNetworkMap) {
constructor(networkMap: NetworkMap, signer: (SerializedBytes<NetworkMap>) -> DigitalSignatureWithCert) : this(networkMap, networkMap.signWithCert(signer))
constructor(signed: SignedNetworkMap) : this(signed.verified(), signed)
operator fun component1(): NetworkMap = networkMap
operator fun component2(): SignedNetworkMap = signed
}

View File

@ -106,10 +106,16 @@ class SchemaMigration(
/** For existing database created before verions 4.0 add Liquibase support - creates DATABASECHANGELOG and DATABASECHANGELOGLOCK tables and mark changesets are executed. */
private fun migrateOlderDatabaseToUseLiquibase(existingCheckpoints: Boolean): Boolean {
//workaround to detect that if Corda finance module is in use then the most recent version with Liquibase migration scripts was deployed
if (schemas.any { schema ->
(schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1")
&& schema.migrationResource == null
})
throw DatabaseMigrationException("Detected incompatible corda-finance cordapp without database migration scripts, replace the existing corda-finance-VERSION.jar with the latest one.")
val isExistingDBWithoutLiquibase = dataSource.connection.use {
it.metaData.getTables(null, null, "NODE%", null).next() &&
!it.metaData.getTables(null, null, "DATABASECHANGELOG", null).next() &&
!it.metaData.getTables(null, null, "DATABASECHANGELOGLOCK", null).next()
(it.metaData.getTables(null, null, "NODE%", null).next() &&
!it.metaData.getTables(null, null, "DATABASECHANGELOG%", null).next())
}
when {
isExistingDBWithoutLiquibase && existingCheckpoints -> throw CheckpointsException()
@ -119,29 +125,31 @@ class SchemaMigration(
dataSource.connection.use { connection ->
// Schema migrations pre release 4.0
val preV4Baseline =
listOf("migration/common.changelog-init.xml",
"migration/node-info.changelog-init.xml",
"migration/node-info.changelog-v1.xml",
"migration/node-info.changelog-v2.xml",
"migration/node-core.changelog-init.xml",
"migration/node-core.changelog-v3.xml",
"migration/node-core.changelog-v4.xml",
"migration/node-core.changelog-v5.xml",
"migration/node-core.changelog-pkey.xml",
"migration/vault-schema.changelog-init.xml",
"migration/vault-schema.changelog-v3.xml",
"migration/vault-schema.changelog-v4.xml",
"migration/vault-schema.changelog-pkey.xml",
"migration/cash.changelog-init.xml",
"migration/cash.changelog-v1.xml",
"migration/commercial-paper.changelog-init.xml",
"migration/commercial-paper.changelog-v1.xml") +
if (schemas.any { schema -> schema.migrationResource == "node-notary.changelog-master" })
listOf("migration/node-notary.changelog-init.xml",
"migration/node-notary.changelog-v1.xml",
"migration/vault-schema.changelog-pkey.xml")
else emptyList()
val preV4Baseline = mutableListOf("migration/common.changelog-init.xml",
"migration/node-info.changelog-init.xml",
"migration/node-info.changelog-v1.xml",
"migration/node-info.changelog-v2.xml",
"migration/node-core.changelog-init.xml",
"migration/node-core.changelog-v3.xml",
"migration/node-core.changelog-v4.xml",
"migration/node-core.changelog-v5.xml",
"migration/node-core.changelog-pkey.xml",
"migration/vault-schema.changelog-init.xml",
"migration/vault-schema.changelog-v3.xml",
"migration/vault-schema.changelog-v4.xml",
"migration/vault-schema.changelog-pkey.xml")
if (schemas.any { schema -> schema.migrationResource == "cash.changelog-master" })
preV4Baseline.addAll(listOf("migration/cash.changelog-init.xml",
"migration/cash.changelog-v1.xml"))
if (schemas.any { schema -> schema.migrationResource == "commercial-paper.changelog-master" })
preV4Baseline.addAll(listOf("migration/commercial-paper.changelog-init.xml",
"migration/commercial-paper.changelog-v1.xml"))
if (schemas.any { schema -> schema.migrationResource == "node-notary.changelog-master" })
preV4Baseline.addAll(listOf("migration/node-notary.changelog-init.xml",
"migration/node-notary.changelog-v1.xml"))
val customResourceAccessor = CustomResourceAccessor(dynamicInclude, preV4Baseline, classLoader)
val liquibase = Liquibase(dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection(connection)))

View File

@ -116,14 +116,14 @@ class AMQPClient(val targets: List<NetworkHostAndPort>,
private val conf = parent.configuration
init {
keyManagerFactory.init(conf.keyStore, conf.keyStorePrivateKeyPassword)
keyManagerFactory.init(conf.keyStore)
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail))
}
override fun initChannel(ch: SocketChannel) {
val pipeline = ch.pipeline()
val target = parent.currentTarget
val handler = createClientSslHelper(target, keyManagerFactory, trustManagerFactory)
val handler = createClientSslHelper(target, parent.allowedRemoteLegalNames, keyManagerFactory, trustManagerFactory)
pipeline.addLast("sslHandler", handler)
if (conf.trace) pipeline.addLast("logger", LoggingHandler(LogLevel.INFO))
pipeline.addLast(AMQPChannelHandler(false,

View File

@ -1,6 +1,7 @@
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 {
@ -21,19 +22,14 @@ interface AMQPConfiguration {
get() = ArtemisMessagingComponent.PEER_USER
/**
* The keystore used for TLS connections
* The key store used for TLS connections
*/
val keyStore: KeyStore
val keyStore: CertificateStore
/**
* Password used to unlock TLS private keys in the KeyStore.
* The trust root key store to validate the peer certificates against
*/
val keyStorePrivateKeyPassword: CharArray
/**
* The trust root KeyStore to validate the peer certificates against
*/
val trustStore: KeyStore
val trustStore: CertificateStore
/**
* Setting crlCheckSoftFail to true allows certificate paths where some leaf certificates do not contain cRLDistributionPoints

View File

@ -60,7 +60,7 @@ class AMQPServer(val hostName: String,
private val conf = parent.configuration
init {
keyManagerFactory.init(conf.keyStore, conf.keyStorePrivateKeyPassword)
keyManagerFactory.init(conf.keyStore)
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, conf.crlCheckSoftFail))
}

View File

@ -1,22 +1,25 @@
package net.corda.nodeapi.internal.protonwrapper.netty
import io.netty.handler.ssl.SslHandler
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.toHex
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.internal.ArtemisTcpTransport
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.toBc
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier
import java.net.Socket
import java.security.KeyStore
import java.security.SecureRandom
import java.security.cert.*
import java.util.*
import javax.net.ssl.*
private const val HOSTNAME_FORMAT = "%s.corda.net"
internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager) : X509ExtendedTrustManager() {
companion object {
val log = contextLogger()
@ -103,6 +106,7 @@ internal class LoggingTrustManagerWrapper(val wrapped: X509ExtendedTrustManager)
}
internal fun createClientSslHelper(target: NetworkHostAndPort,
expectedRemoteLegalNames: Set<CordaX500Name>,
keyManagerFactory: KeyManagerFactory,
trustManagerFactory: TrustManagerFactory): SslHandler {
val sslContext = SSLContext.getInstance("TLS")
@ -114,6 +118,11 @@ internal fun createClientSslHelper(target: NetworkHostAndPort,
sslEngine.enabledProtocols = ArtemisTcpTransport.TLS_VERSIONS.toTypedArray()
sslEngine.enabledCipherSuites = ArtemisTcpTransport.CIPHER_SUITES.toTypedArray()
sslEngine.enableSessionCreation = true
if (expectedRemoteLegalNames.size == 1) {
val sslParameters = sslEngine.sslParameters
sslParameters.serverNames = listOf(SNIHostName(x500toHostName(expectedRemoteLegalNames.single())))
sslEngine.sslParameters = sslParameters
}
return SslHandler(sslEngine)
}
@ -132,7 +141,7 @@ internal fun createServerSslHelper(keyManagerFactory: KeyManagerFactory,
return SslHandler(sslEngine)
}
internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: KeyStore, crlCheckSoftFail: Boolean): ManagerFactoryParameters {
internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore, crlCheckSoftFail: Boolean): ManagerFactoryParameters {
val certPathBuilder = CertPathBuilder.getInstance("PKIX")
val revocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker
revocationChecker.options = EnumSet.of(
@ -145,7 +154,18 @@ internal fun initialiseTrustStoreAndEnableCrlChecking(trustStore: KeyStore, crlC
// the following reasons: The CRL or OCSP response cannot be obtained because of a network error.
revocationChecker.options = revocationChecker.options + PKIXRevocationChecker.Option.SOFT_FAIL
}
val pkixParams = PKIXBuilderParameters(trustStore, X509CertSelector())
val pkixParams = PKIXBuilderParameters(trustStore.value.internal, X509CertSelector())
pkixParams.addCertPathChecker(revocationChecker)
return CertPathTrustManagerParameters(pkixParams)
}
fun KeyManagerFactory.init(keyStore: CertificateStore) = init(keyStore.value.internal, keyStore.password.toCharArray())
fun TrustManagerFactory.init(trustStore: CertificateStore) = init(trustStore.value.internal)
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
// representation of the SHA256 hash is truncated to 32 characters.
return String.format(HOSTNAME_FORMAT, secureHash.toString().substring(0..32).toLowerCase())
}

View File

@ -15,6 +15,7 @@ import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.node.VersionInfo
import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.internal.cordapp.JarScanningCordappLoader
@ -110,7 +111,7 @@ class AttachmentsClassLoaderStaticContractTests {
val cordapps = cordappsForPackages(packages)
return testDirectory().let { directory ->
cordapps.packageInDirectory(directory)
JarScanningCordappLoader.fromDirectories(listOf(directory))
JarScanningCordappLoader.fromDirectories(listOf(directory), VersionInfo.UNKNOWN)
}
}

View File

@ -1,8 +1,7 @@
package net.corda.nodeapi.internal.crypto
import net.corda.core.internal.validate
import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_FILE
import net.corda.nodeapi.internal.DEV_CA_TRUST_STORE_PASS
import net.corda.nodeapi.internal.loadDevCaTrustStore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@ -22,8 +21,8 @@ class DevCertificatesTest {
@Test
fun `create server certificate in keystore for SSL`() {
// given
val newTrustStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/$DEV_CA_TRUST_STORE_FILE"), DEV_CA_TRUST_STORE_PASS)
val newTrustRoot = newTrustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
val newTrustStore = loadDevCaTrustStore()
val newTrustRoot = newTrustStore[X509Utilities.CORDA_ROOT_CA]
val newTrustAnchor = TrustAnchor(newTrustRoot, null)
val oldNodeCaKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("regression-test/$OLD_NODE_DEV_KEYSTORE_FILE_NAME"), OLD_DEV_KEYSTORE_PASS)

View File

@ -1,17 +1,26 @@
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.*
import net.corda.core.crypto.Crypto.COMPOSITE_KEY
import net.corda.core.crypto.Crypto.ECDSA_SECP256K1_SHA256
import net.corda.core.crypto.Crypto.ECDSA_SECP256R1_SHA256
import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512
import net.corda.core.crypto.Crypto.RSA_SHA256
import net.corda.core.crypto.Crypto.SPHINCS256_SHA256
import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.crypto.newSecureRandom
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.createDevKeyStores
import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.createDevNodeCa
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME
import net.corda.nodeapi.internal.protonwrapper.netty.init
import net.corda.nodeapi.internal.registerDevP2pCertificates
import net.corda.nodeapi.internal.registerDevSigningCertificates
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.SerializationContextImpl
import net.corda.serialization.internal.SerializationFactoryImpl
@ -19,19 +28,26 @@ import net.corda.serialization.internal.amqp.amqpMagic
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.stubs.CertificateStoreStubs
import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import sun.security.rsa.RSAPrivateCrtKeyImpl
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.nio.file.Path
import java.security.SecureRandom
import java.security.Key
import java.security.KeyPair
import java.security.PrivateKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.util.*
@ -47,8 +63,29 @@ class X509UtilitiesTest {
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
val CIPHER_SUITES = arrayOf(
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
)
// We ensure that all of the algorithms are both used (at least once) as first and second in the following [Pair]s.
// We also add [DEFAULT_TLS_SIGNATURE_SCHEME] and [DEFAULT_IDENTITY_SIGNATURE_SCHEME] combinations for consistency.
val certChainSchemeCombinations = listOf(
Pair(DEFAULT_TLS_SIGNATURE_SCHEME, DEFAULT_TLS_SIGNATURE_SCHEME),
Pair(DEFAULT_IDENTITY_SIGNATURE_SCHEME, DEFAULT_IDENTITY_SIGNATURE_SCHEME),
Pair(DEFAULT_TLS_SIGNATURE_SCHEME, DEFAULT_IDENTITY_SIGNATURE_SCHEME),
Pair(ECDSA_SECP256R1_SHA256, SPHINCS256_SHA256),
Pair(ECDSA_SECP256K1_SHA256, RSA_SHA256),
Pair(EDDSA_ED25519_SHA512, ECDSA_SECP256K1_SHA256),
Pair(RSA_SHA256, EDDSA_ED25519_SHA512),
Pair(SPHINCS256_SHA256, ECDSA_SECP256R1_SHA256)
)
val schemeToKeyTypes = listOf(
// By default, JKS returns SUN EC key.
Triple(ECDSA_SECP256R1_SHA256,java.security.interfaces.ECPrivateKey::class.java, org.bouncycastle.jce.interfaces.ECPrivateKey::class.java),
Triple(ECDSA_SECP256K1_SHA256,java.security.interfaces.ECPrivateKey::class.java, org.bouncycastle.jce.interfaces.ECPrivateKey::class.java),
Triple(EDDSA_ED25519_SHA512, EdDSAPrivateKey::class.java, EdDSAPrivateKey::class.java),
// By default, JKS returns SUN RSA key.
Triple(RSA_SHA256, RSAPrivateCrtKeyImpl::class.java, BCRSAPrivateCrtKey::class.java),
Triple(SPHINCS256_SHA256, BCSphincs256PrivateKey::class.java, BCSphincs256PrivateKey::class.java)
)
}
@ -58,7 +95,11 @@ class X509UtilitiesTest {
@Test
fun `create valid self-signed CA certificate`() {
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { validSelfSignedCertificate(it) }
}
private fun validSelfSignedCertificate(signatureScheme: SignatureScheme) {
val caKey = generateKeyPair(signatureScheme)
val subject = X500Principal("CN=Test Cert,O=R3 Ltd,L=London,C=GB")
val caCert = X509Utilities.createSelfSignedCACertificate(subject, caKey)
assertEquals(subject, caCert.subjectX500Principal) // using our subject common name
@ -75,8 +116,12 @@ class X509UtilitiesTest {
@Test
fun `load and save a PEM file certificate`() {
Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { loadSavePEMCert(it) }
}
private fun loadSavePEMCert(signatureScheme: SignatureScheme) {
val tmpCertificateFile = tempFile("cacert.pem")
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val caKey = generateKeyPair(signatureScheme)
val caCert = X509Utilities.createSelfSignedCACertificate(X500Principal("CN=Test Cert,O=R3 Ltd,L=London,C=GB"), caKey)
X509Utilities.saveCertificateAsPEMFile(caCert, tmpCertificateFile)
val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile)
@ -85,29 +130,52 @@ class X509UtilitiesTest {
@Test
fun `create valid server certificate chain`() {
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val caCert = X509Utilities.createSelfSignedCACertificate(X500Principal("CN=Test CA Cert,O=R3 Ltd,L=London,C=GB"), caKey)
val subject = X500Principal("CN=Server Cert,O=R3 Ltd,L=London,C=GB")
val keyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subject, keyPair.public)
assertEquals(subject, serverCert.subjectX500Principal) // using our subject common name
assertEquals(caCert.issuerX500Principal, serverCert.issuerX500Principal) // Issued by our CA cert
serverCert.checkValidity(Date()) // throws on verification problems
serverCert.verify(caKey.public) // throws on verification problems
serverCert.toBc().run {
certChainSchemeCombinations.forEach { createValidServerCertChain(it.first, it.second) }
}
private fun createValidServerCertChain(signatureSchemeRoot: SignatureScheme, signatureSchemeChild: SignatureScheme) {
val (caKeyPair, caCert, _, childCert, _, childSubject)
= genCaAndChildKeysCertsAndSubjects(signatureSchemeRoot, signatureSchemeChild)
assertEquals(childSubject, childCert.subjectX500Principal) // Using our subject common name.
assertEquals(caCert.issuerX500Principal, childCert.issuerX500Principal) // Issued by our CA cert.
childCert.checkValidity(Date()) // Throws on verification problems.
childCert.verify(caKeyPair.public) // Throws on verification problems.
childCert.toBc().run {
val basicConstraints = BasicConstraints.getInstance(getExtension(Extension.basicConstraints).parsedValue)
val keyUsage = KeyUsage.getInstance(getExtension(Extension.keyUsage).parsedValue)
assertFalse { keyUsage.hasUsages(5) } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property)
assertNull(basicConstraints.pathLenConstraint) // Non-CA certificate
assertFalse { keyUsage.hasUsages(5) } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property).
assertNull(basicConstraints.pathLenConstraint) // Non-CA certificate.
}
}
private data class CaAndChildKeysCertsAndSubjects(val caKeyPair: KeyPair,
val caCert: X509Certificate,
val childKeyPair: KeyPair,
val childCert: X509Certificate,
val caSubject: X500Principal,
val childSubject: X500Principal)
private fun genCaAndChildKeysCertsAndSubjects(signatureSchemeRoot: SignatureScheme,
signatureSchemeChild: SignatureScheme,
rootSubject: X500Principal = X500Principal("CN=Test CA Cert,O=R3 Ltd,L=London,C=GB"),
childSubject: X500Principal = X500Principal("CN=Test Child Cert,O=R3 Ltd,L=London,C=GB")): CaAndChildKeysCertsAndSubjects {
val caKeyPair = generateKeyPair(signatureSchemeRoot)
val caCert = X509Utilities.createSelfSignedCACertificate(rootSubject, caKeyPair)
val childKeyPair = generateKeyPair(signatureSchemeChild)
val childCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKeyPair, childSubject, childKeyPair.public)
return CaAndChildKeysCertsAndSubjects(caKeyPair, caCert, childKeyPair, childCert, rootSubject, childSubject)
}
@Test
fun `create valid server certificate chain includes CRL info`() {
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
certChainSchemeCombinations.forEach { createValidServerCertIncludeCRL(it.first, it.second) }
}
private fun createValidServerCertIncludeCRL(signatureSchemeRoot: SignatureScheme, signatureSchemeChild: SignatureScheme) {
val caKey = generateKeyPair(signatureSchemeRoot)
val caCert = X509Utilities.createSelfSignedCACertificate(X500Principal("CN=Test CA Cert,O=R3 Ltd,L=London,C=GB"), caKey)
val caSubjectKeyIdentifier = SubjectKeyIdentifier.getInstance(caCert.toBc().getExtension(Extension.subjectKeyIdentifier).parsedValue)
val keyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val keyPair = generateKeyPair(signatureSchemeChild)
val crlDistPoint = "http://test.com"
val serverCert = X509Utilities.createCertificate(
CertificateType.TLS,
@ -125,84 +193,58 @@ class X509UtilitiesTest {
}
@Test
fun `storing EdDSA key in java keystore`() {
fun `storing all supported key types in java keystore`() {
Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { storeKeyToKeystore(it) }
}
private fun storeKeyToKeystore(signatureScheme: SignatureScheme) {
val tmpKeyStore = tempFile("keystore.jks")
val keyPair = generateKeyPair(EDDSA_ED25519_SHA512)
val keyPair = generateKeyPair(signatureScheme)
val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB")
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair)
assertTrue(Arrays.equals(selfSignCert.publicKey.encoded, keyPair.public.encoded))
// Save the EdDSA private key with self sign cert in the keystore.
// Save the private key with self sign cert in the keystore.
val keyStore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
keyStore.setKeyEntry("Key", keyPair.private, "password".toCharArray(), arrayOf(selfSignCert))
keyStore.save(tmpKeyStore, "keystorepass")
// Load the keystore from file and make sure keys are intact.
val keyStore2 = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
val privateKey = keyStore2.getKey("Key", "password".toCharArray())
val pubKey = keyStore2.getCertificate("Key").publicKey
val reloadedKeystore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
val reloadedPrivateKey = reloadedKeystore.getKey("Key", "password".toCharArray())
val reloadedPublicKey = reloadedKeystore.getCertificate("Key").publicKey
assertNotNull(pubKey)
assertNotNull(privateKey)
assertEquals(keyPair.public, pubKey)
assertEquals(keyPair.private, privateKey)
}
@Test
fun `signing EdDSA key with EcDSA certificate`() {
val tmpKeyStore = tempFile("keystore.jks")
val ecDSAKey = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB")
val ecDSACert = X509Utilities.createSelfSignedCACertificate(testName, ecDSAKey)
val edDSAKeypair = generateKeyPair(EDDSA_ED25519_SHA512)
val edDSACert = X509Utilities.createCertificate(CertificateType.TLS, ecDSACert, ecDSAKey, BOB.name.x500Principal, edDSAKeypair.public)
// Save the EdDSA private key with cert chains.
val keyStore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
keyStore.setKeyEntry("Key", edDSAKeypair.private, "password".toCharArray(), arrayOf(ecDSACert, edDSACert))
keyStore.save(tmpKeyStore, "keystorepass")
// Load the keystore from file and make sure keys are intact.
val keyStore2 = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
val privateKey = keyStore2.getKey("Key", "password".toCharArray())
val certs = keyStore2.getCertificateChain("Key")
val pubKey = certs.last().publicKey
assertEquals(2, certs.size)
assertNotNull(pubKey)
assertNotNull(privateKey)
assertEquals(edDSAKeypair.public, pubKey)
assertEquals(edDSAKeypair.private, privateKey)
assertNotNull(reloadedPublicKey)
assertNotNull(reloadedPrivateKey)
assertEquals(keyPair.public, reloadedPublicKey)
assertEquals(keyPair.private, reloadedPrivateKey)
}
@Test
fun `create server certificate in keystore for SSL`() {
val sslConfig = object : SSLConfiguration {
override val certificatesDirectory = tempFolder.root.toPath()
override val keyStorePassword = "serverstorepass"
override val trustStorePassword = "trustpass"
override val crlCheckSoftFail: Boolean = true
}
val certificatesDirectory = tempFolder.root.toPath()
val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, "serverstorepass")
val p2pSslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory, keyStorePassword = "serverstorepass")
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
// Generate server cert and private key and populate another keystore suitable for SSL
sslConfig.createDevKeyStores(MEGA_CORP.name, rootCa.certificate, intermediateCa)
val nodeCa = createDevNodeCa(intermediateCa, MEGA_CORP.name)
signingCertStore.get(createNew = true).also { it.registerDevSigningCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) }
p2pSslConfig.keyStore.get(createNew = true).also { it.registerDevP2pCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa, nodeCa) }
// Load back server certificate
val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword)
val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, sslConfig.keyStorePassword)
val serverKeyStore = signingCertStore.get().value
val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
serverCert.checkValidity()
serverCert.verify(intermediateCa.certificate.publicKey)
assertThat(CordaX500Name.build(serverCert.subjectX500Principal)).isEqualTo(MEGA_CORP.name)
// Load back SSL certificate
val sslKeyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
val (sslCert) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslConfig.keyStorePassword)
val sslKeyStoreReloaded = p2pSslConfig.keyStore.get()
val (sslCert) = sslKeyStoreReloaded.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, p2pSslConfig.keyStore.password) }
sslCert.checkValidity()
sslCert.verify(serverCert.publicKey)
@ -216,25 +258,20 @@ class X509UtilitiesTest {
@Test
fun `create server cert and use in SSL socket`() {
val sslConfig = object : SSLConfiguration {
override val certificatesDirectory = tempFolder.root.toPath()
override val keyStorePassword = "serverstorepass"
override val trustStorePassword = "trustpass"
override val crlCheckSoftFail: Boolean = true
}
val sslConfig = CertificateStoreStubs.P2P.withCertificatesDirectory(tempFolder.root.toPath(), keyStorePassword = "serverstorepass")
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
// Generate server cert and private key and populate another keystore suitable for SSL
sslConfig.createDevKeyStores(MEGA_CORP.name, rootCa.certificate, intermediateCa)
sslConfig.keyStore.get(true).registerDevP2pCertificates(MEGA_CORP.name, rootCa.certificate, intermediateCa)
sslConfig.createTrustStore(rootCa.certificate)
val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword)
val keyStore = sslConfig.keyStore.get()
val trustStore = sslConfig.trustStore.get()
val context = SSLContext.getInstance("TLS")
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray())
keyManagerFactory.init(keyStore)
val keyManagers = keyManagerFactory.keyManagers
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustMgrFactory.init(trustStore)
@ -313,15 +350,24 @@ class X509UtilitiesTest {
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) {
val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword)
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
trustStore.save(trustStoreFile, trustStorePassword)
private fun MutualSslConfiguration.createTrustStore(rootCert: X509Certificate) {
val trustStore = this.trustStore.get(true)
trustStore[X509Utilities.CORDA_ROOT_CA] = rootCert
}
@Test
fun `get correct private key type from Keystore`() {
val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
schemeToKeyTypes.forEach { getCorrectKeyFromKeystore(it.first, it.second, it.third) }
}
private fun <U, C> getCorrectKeyFromKeystore(signatureScheme: SignatureScheme, uncastedClass: Class<U>, castedClass: Class<C>) {
val keyPair = generateKeyPair(signatureScheme)
val (keyFromKeystore, keyFromKeystoreCasted) = storeAndGetKeysFromKeystore(keyPair)
assertThat(keyFromKeystore).isInstanceOf(uncastedClass)
assertThat(keyFromKeystoreCasted).isInstanceOf(castedClass)
}
private fun storeAndGetKeysFromKeystore(keyPair: KeyPair): Pair<Key, PrivateKey> {
val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB")
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair)
val keyStore = loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword")
@ -329,13 +375,15 @@ class X509UtilitiesTest {
val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray())
val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword")
assertTrue(keyFromKeystore is java.security.interfaces.ECPrivateKey) // by default JKS returns SUN EC key
assertTrue(keyFromKeystoreCasted is org.bouncycastle.jce.interfaces.ECPrivateKey)
return Pair(keyFromKeystore, keyFromKeystoreCasted)
}
@Test
fun `serialize - deserialize X509Certififcate`() {
fun `serialize - deserialize X509Certificate`() {
Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { serializeDeserializeX509Cert(it) }
}
private fun serializeDeserializeX509Cert(signatureScheme: SignatureScheme) {
val factory = SerializationFactoryImpl().apply { registerScheme(AMQPServerSerializationScheme()) }
val context = SerializationContextImpl(amqpMagic,
javaClass.classLoader,
@ -344,7 +392,7 @@ class X509UtilitiesTest {
true,
SerializationContext.UseCase.P2P,
null)
val expected = X509Utilities.createSelfSignedCACertificate(ALICE.name.x500Principal, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
val expected = X509Utilities.createSelfSignedCACertificate(ALICE.name.x500Principal, generateKeyPair(signatureScheme))
val serialized = expected.serialize(factory, context).bytes
val actual = serialized.deserialize<X509Certificate>(factory, context)
assertEquals(expected, actual)
@ -352,6 +400,10 @@ class X509UtilitiesTest {
@Test
fun `serialize - deserialize X509CertPath`() {
Crypto.supportedSignatureSchemes().filter { it != COMPOSITE_KEY }.forEach { serializeDeserializeX509CertPath(it) }
}
private fun serializeDeserializeX509CertPath(signatureScheme: SignatureScheme) {
val factory = SerializationFactoryImpl().apply { registerScheme(AMQPServerSerializationScheme()) }
val context = SerializationContextImpl(
amqpMagic,
@ -362,7 +414,7 @@ class X509UtilitiesTest {
SerializationContext.UseCase.P2P,
null
)
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCAKey = generateKeyPair(signatureScheme)
val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE_NAME.x500Principal, rootCAKey)
val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB_NAME.x500Principal, BOB.publicKey)
val expected = X509Utilities.buildCertPath(certificate, rootCACert)
@ -370,4 +422,33 @@ class X509UtilitiesTest {
val actual: CertPath = serialized.deserialize(factory, context)
assertEquals(expected, actual)
}
@Test
fun `signing a key type with another key type certificate then store and reload correctly from keystore`() {
certChainSchemeCombinations.forEach { signCertWithOtherKeyTypeAndTestKeystoreReload(it.first, it.second) }
}
private fun signCertWithOtherKeyTypeAndTestKeystoreReload(signatureSchemeRoot: SignatureScheme, signatureSchemeChild: SignatureScheme) {
val tmpKeyStore = tempFile("keystore.jks")
val (_, caCert, childKeyPair, childCert) = genCaAndChildKeysCertsAndSubjects(signatureSchemeRoot, signatureSchemeChild)
// Save the child private key with cert chains.
val keyStore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
keyStore.setKeyEntry("Key", childKeyPair.private, "password".toCharArray(), arrayOf(caCert, childCert))
keyStore.save(tmpKeyStore, "keystorepass")
// Load the keystore from file and make sure keys are intact.
val reloadedKeystore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
val reloadedPrivateKey = reloadedKeystore.getKey("Key", "password".toCharArray())
val reloadedCerts = reloadedKeystore.getCertificateChain("Key")
val reloadedPublicKey = reloadedCerts.last().publicKey
assertEquals(2, reloadedCerts.size)
assertNotNull(reloadedPublicKey)
assertNotNull(reloadedPrivateKey)
assertEquals(childKeyPair.public, reloadedPublicKey)
assertEquals(childKeyPair.private, reloadedPrivateKey)
}
}