Merge remote-tracking branch 'open/master' into shams-os-merge-020118

# Conflicts:
#	client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java
#	node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt
#	node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt
#	node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt
#	node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt
#	node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt
#	node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt
#	testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt
This commit is contained in:
Shams Asari 2018-01-02 15:32:32 +00:00
commit 52173219c3
57 changed files with 1376 additions and 870 deletions

View File

@ -110,7 +110,11 @@ object Crypto {
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
)
/** EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing. */
/**
* EdDSA signature scheme using the ed25519 twisted Edwards curve and SHA512 for message hashing.
* The actual algorithm is PureEdDSA Ed25519 as defined in https://tools.ietf.org/html/rfc8032
* Not to be confused with the EdDSA variants, Ed25519ctx and Ed25519ph.
*/
@JvmField
val EDDSA_ED25519_SHA512 = SignatureScheme(
4,

View File

@ -11,6 +11,8 @@ import net.corda.core.serialization.CordaSerializable
@DoNotImplement
interface FlowLogicRefFactory {
fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*>
}
@CordaSerializable

View File

@ -27,8 +27,8 @@ import kotlin.test.*
*/
class CryptoUtilsTest {
val testString = "Hello World"
val testBytes = testString.toByteArray()
private val testString = "Hello World"
private val testBytes = testString.toByteArray()
// key generation test
@Test

View File

@ -0,0 +1,190 @@
package net.corda.core.crypto
import net.corda.core.utilities.hexToByteArray
import net.corda.core.utilities.toHex
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSASecurityProvider
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import org.junit.Test
import java.security.PrivateKey
import java.security.Signature
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
/**
* Testing PureEdDSA Ed25519 using test vectors from https://tools.ietf.org/html/rfc8032#section-7.1
*/
class EdDSATests {
@Test
fun `PureEdDSA Ed25519 test vectors`() {
val edParams = Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec
// MESSAGE (length 0 bytes).
val testVector1 = SignatureTestVector(
"9d61b19deffd5a60ba844af492ec2cc4" +
"4449c5697b326919703bac031cae7f60",
"d75a980182b10ab7d54bfed3c964073a" +
"0ee172f3daa62325af021a68f707511a",
"",
"e5564300c360ac729086e2cc806e828a" +
"84877f1eb8e5d974d873e06522490155" +
"5fb8821590a33bacc61e39701cf9b46b" +
"d25bf5f0595bbe24655141438e7a100b"
)
// MESSAGE (length 1 byte).
val testVector2 = SignatureTestVector(
"4ccd089b28ff96da9db6c346ec114e0f" +
"5b8a319f35aba624da8cf6ed4fb8a6fb",
"3d4017c3e843895a92b70aa74d1b7ebc" +
"9c982ccf2ec4968cc0cd55f12af4660c",
"72",
"92a009a9f0d4cab8720e820b5f642540" +
"a2b27b5416503f8fb3762223ebdb69da" +
"085ac1e43e15996e458f3613d0f11d8c" +
"387b2eaeb4302aeeb00d291612bb0c00"
)
// MESSAGE (length 2 bytes).
val testVector3 = SignatureTestVector(
"c5aa8df43f9f837bedb7442f31dcb7b1" +
"66d38535076f094b85ce3a2e0b4458f7",
"fc51cd8e6218a1a38da47ed00230f058" +
"0816ed13ba3303ac5deb911548908025",
"af82",
"6291d657deec24024827e69c3abe01a3" +
"0ce548a284743a445e3680d7db5ac3ac" +
"18ff9b538d16f290ae67f760984dc659" +
"4a7c15e9716ed28dc027beceea1ec40a"
)
// MESSAGE (length 1023 bytes).
val testVector1024 = SignatureTestVector(
"f5e5767cf153319517630f226876b86c" +
"8160cc583bc013744c6bf255f5cc0ee5",
"278117fc144c72340f67d0f2316e8386" +
"ceffbf2b2428c9c51fef7c597f1d426e",
"08b8b2b733424243760fe426a4b54908" +
"632110a66c2f6591eabd3345e3e4eb98" +
"fa6e264bf09efe12ee50f8f54e9f77b1" +
"e355f6c50544e23fb1433ddf73be84d8" +
"79de7c0046dc4996d9e773f4bc9efe57" +
"38829adb26c81b37c93a1b270b20329d" +
"658675fc6ea534e0810a4432826bf58c" +
"941efb65d57a338bbd2e26640f89ffbc" +
"1a858efcb8550ee3a5e1998bd177e93a" +
"7363c344fe6b199ee5d02e82d522c4fe" +
"ba15452f80288a821a579116ec6dad2b" +
"3b310da903401aa62100ab5d1a36553e" +
"06203b33890cc9b832f79ef80560ccb9" +
"a39ce767967ed628c6ad573cb116dbef" +
"efd75499da96bd68a8a97b928a8bbc10" +
"3b6621fcde2beca1231d206be6cd9ec7" +
"aff6f6c94fcd7204ed3455c68c83f4a4" +
"1da4af2b74ef5c53f1d8ac70bdcb7ed1" +
"85ce81bd84359d44254d95629e9855a9" +
"4a7c1958d1f8ada5d0532ed8a5aa3fb2" +
"d17ba70eb6248e594e1a2297acbbb39d" +
"502f1a8c6eb6f1ce22b3de1a1f40cc24" +
"554119a831a9aad6079cad88425de6bd" +
"e1a9187ebb6092cf67bf2b13fd65f270" +
"88d78b7e883c8759d2c4f5c65adb7553" +
"878ad575f9fad878e80a0c9ba63bcbcc" +
"2732e69485bbc9c90bfbd62481d9089b" +
"eccf80cfe2df16a2cf65bd92dd597b07" +
"07e0917af48bbb75fed413d238f5555a" +
"7a569d80c3414a8d0859dc65a46128ba" +
"b27af87a71314f318c782b23ebfe808b" +
"82b0ce26401d2e22f04d83d1255dc51a" +
"ddd3b75a2b1ae0784504df543af8969b" +
"e3ea7082ff7fc9888c144da2af58429e" +
"c96031dbcad3dad9af0dcbaaaf268cb8" +
"fcffead94f3c7ca495e056a9b47acdb7" +
"51fb73e666c6c655ade8297297d07ad1" +
"ba5e43f1bca32301651339e22904cc8c" +
"42f58c30c04aafdb038dda0847dd988d" +
"cda6f3bfd15c4b4c4525004aa06eeff8" +
"ca61783aacec57fb3d1f92b0fe2fd1a8" +
"5f6724517b65e614ad6808d6f6ee34df" +
"f7310fdc82aebfd904b01e1dc54b2927" +
"094b2db68d6f903b68401adebf5a7e08" +
"d78ff4ef5d63653a65040cf9bfd4aca7" +
"984a74d37145986780fc0b16ac451649" +
"de6188a7dbdf191f64b5fc5e2ab47b57" +
"f7f7276cd419c17a3ca8e1b939ae49e4" +
"88acba6b965610b5480109c8b17b80e1" +
"b7b750dfc7598d5d5011fd2dcc5600a3" +
"2ef5b52a1ecc820e308aa342721aac09" +
"43bf6686b64b2579376504ccc493d97e" +
"6aed3fb0f9cd71a43dd497f01f17c0e2" +
"cb3797aa2a2f256656168e6c496afc5f" +
"b93246f6b1116398a346f1a641f3b041" +
"e989f7914f90cc2c7fff357876e506b5" +
"0d334ba77c225bc307ba537152f3f161" +
"0e4eafe595f6d9d90d11faa933a15ef1" +
"369546868a7f3a45a96768d40fd9d034" +
"12c091c6315cf4fde7cb68606937380d" +
"b2eaaa707b4c4185c32eddcdd306705e" +
"4dc1ffc872eeee475a64dfac86aba41c" +
"0618983f8741c5ef68d3a101e8a3b8ca" +
"c60c905c15fc910840b94c00a0b9d0",
"0aab4c900501b3e24d7cdf4663326a3a" +
"87df5e4843b2cbdb67cbf6e460fec350" +
"aa5371b1508f9f4528ecea23c436d94b" +
"5e8fcd4f681e30a6ac00a9704a188a03"
)
// MESSAGE (length 64 bytes). TEST SHA(abc).
val testVectorSHAabc = SignatureTestVector(
"833fe62409237b9d62ec77587520911e" +
"9a759cec1d19755b7da901b96dca3d42",
"ec172b93ad5e563bf4932c70e1245034" +
"c35467ef2efd4d64ebf819683467e2bf",
"ddaf35a193617abacc417349ae204131" +
"12e6fa4e89a97ea20a9eeee64b55d39a" +
"2192992a274fc1a836ba3c23a3feebbd" +
"454d4423643ce80e2a9ac94fa54ca49f",
"dc2a4459e7369633a52b1bf277839a00" +
"201009a3efbf3ecb69bea2186c26b589" +
"09351fc9ac90b3ecfdfbc7c66431e030" +
"3dca179c138ac17ad9bef1177331a704"
)
val testVectors = listOf(testVector1, testVector2, testVector3, testVector1024, testVectorSHAabc)
testVectors.forEach {
val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(it.privateKeyHex.hexToByteArray(), edParams))
assertEquals(it.signatureOutputHex, doSign(privateKey, it.messageToSignHex.hexToByteArray()).toHex().toLowerCase())
}
// Test vector for the variant Ed25519ctx, expected to fail.
val testVectorEd25519ctx = SignatureTestVector(
"0305334e381af78f141cb666f6199f57" +
"bc3495335a256a95bd2a55bf546663f6",
"dfc9425e4f968f7f0c29f0259cf5f9ae" +
"d6851c2bb4ad8bfb860cfee0ab248292",
"f726936d19c800494e3fdaff20b276a8",
"fc60d5872fc46b3aa69f8b5b4351d580" +
"8f92bcc044606db097abab6dbcb1aee3" +
"216c48e8b3b66431b5b186d1d28f8ee1" +
"5a5ca2df6668346291c2043d4eb3e90d"
)
val privateKey = EdDSAPrivateKey(EdDSAPrivateKeySpec(testVectorEd25519ctx.privateKeyHex.hexToByteArray(), edParams))
assertNotEquals(testVectorEd25519ctx.signatureOutputHex, doSign(privateKey, testVectorEd25519ctx.messageToSignHex.hexToByteArray()).toHex().toLowerCase())
}
/** A test vector object for digital signature schemes. */
private data class SignatureTestVector(val privateKeyHex: String,
val publicKeyHex: String,
val messageToSignHex: String,
val signatureOutputHex: String)
// Required to implement a custom doSign function, because Corda's Crypto.doSign does not allow empty messages (testVector1).
private fun doSign(privateKey: PrivateKey, clearData: ByteArray): ByteArray {
val signature = Signature.getInstance(Crypto.EDDSA_ED25519_SHA512.signatureName, EdDSASecurityProvider())
signature.initSign(privateKey)
signature.update(clearData)
return signature.sign()
}
}

View File

@ -1,9 +1,9 @@
package net.corda.core.crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.toTypedArray
import net.corda.core.internal.cert
import net.corda.nodeapi.internal.crypto.*
import net.corda.testing.internal.createDevIntermediateCaCertPath
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
@ -11,35 +11,40 @@ import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.Test
import java.security.KeyStore
import java.security.cert.*
import java.util.stream.Stream
import java.security.cert.CertPathValidator
import java.security.cert.CertPathValidatorException
import java.security.cert.PKIXParameters
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class X509NameConstraintsTest {
private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair<KeyStore, KeyStore> {
val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Corda Root CA", organisation = "R3 Ltd", locality = "London", country = "GB"), rootKeys)
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, CordaX500Name(commonName = "Corda Intermediate CA", organisation = "R3 Ltd", locality = "London", country = "GB"), intermediateCAKeyPair.public)
val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, CordaX500Name(commonName = "Corda Client CA", organisation = "R3 Ltd", locality = "London", country = "GB"), clientCAKeyPair.public, nameConstraints = nameConstraints)
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val nodeCaCert = X509Utilities.createCertificate(
CertificateType.NODE_CA,
intermediateCa.certificate,
intermediateCa.keyPair,
CordaX500Name("Corda Client CA", "R3 Ltd", "London", "GB"),
nodeCaKeyPair.public,
nameConstraints = nameConstraints)
val keyPass = "password"
val trustStore = KeyStore.getInstance(KEYSTORE_TYPE)
trustStore.load(null, keyPass.toCharArray())
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert)
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa.certificate.cert)
val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientCAKeyPair, subjectName, tlsKey.public)
val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, subjectName, tlsKeyPair.public)
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
keyStore.load(null, keyPass.toCharArray())
keyStore.addOrReplaceKey(X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, keyPass.toCharArray(),
Stream.of(tlsCert, clientCACert, intermediateCACert, rootCACert).map { it.cert }.toTypedArray<Certificate>())
keyStore.addOrReplaceKey(
X509Utilities.CORDA_CLIENT_TLS,
tlsKeyPair.private,
keyPass.toCharArray(),
arrayOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCa.certificate))
return Pair(keyStore, trustStore)
}

View File

@ -28,18 +28,16 @@ Setting your dependencies
Choosing your Corda version
^^^^^^^^^^^^^^^^^^^^^^^^^^^
The following two lines of the ``build.gradle`` file define the Corda version used to build your CorDapp:
``ext.corda_release_version`` and ``ext.corda_gradle_plugins_version`` are used in the ``build.gradle`` to define the
versions of Corda and the Corda Gradle Plugins that are used to build your CorDapp.
For example, to use version 1.0 of Corda and version 1.0 of the Corda gradle plugins, you'd write:
.. sourcecode:: groovy
ext.corda_release_version = '1.0.0'
ext.corda_gradle_plugins_version = '1.0.0'
In this case, our CorDapp will use:
* Version 1.0 of Corda
* Version 1.0 of the Corda gradle plugins
You can find the latest published version of both here: https://bintray.com/r3/corda.
``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and
@ -50,25 +48,32 @@ In certain cases, you may also wish to build against the unstable Master branch.
Corda dependencies
^^^^^^^^^^^^^^^^^^
The ``cordformation`` plugin adds:
The ``cordformation`` plugin adds two new gradle configurations:
* ``cordaCompile`` as a new configuration that ``compile`` extends from
* ``cordaRuntime`` which ``runtime`` extends from
* ``cordaCompile``, which extends ``compile``
* ``cordaRuntime``, which extends ``runtime``
To build against Corda you must add the following to your ``build.gradle`` file;
To build against Corda, you must add the following to your ``build.gradle`` file:
* The ``net.corda:corda:<version>`` JAR as a ``cordaRuntime`` dependency
* Each compile dependency (eg ``corda-core``) as a ``cordaCompile`` dependency
* ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency
* Each Corda compile dependency (eg ``net.corda:corda-core:$corda_release_version``) as a ``cordaCompile`` dependency
To use Corda's test facilities you must add ``net.corda:corda-test-utils:<version>`` as a ``testCompile`` dependency
(i.e. a default Java/Kotlin test compile task).
You may also want to add:
* ``net.corda:corda-test-utils:$corda_release_version`` as a ``testCompile`` dependency, in order to use Corda's test
frameworks
* ``net.corda:corda-webserver:$corda_release_version`` as a ``cordaRuntime`` dependency, in order to use Corda's
built-in development webserver
.. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency.
Dependencies on other CorDapps
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Sometimes, a CorDapp you build will depend on states, contracts or flows defined in another CorDapp. You must include
the CorDapp your CorDapp depends upon as a ``cordapp`` dependency in your ``build.gradle`` file.
You CorDapp may also depend on classes defined in another CorDapp, such as states, contracts and flows. There are two
ways to add another CorDapp as a dependency in your CorDapp's ``build.gradle`` file:
* ``cordapp project(":another-cordapp")`` (use this if the other CorDapp is defined in a module in the same project)
* ``cordapp "net.corda:another-cordapp:1.0"`` (use this otherwise)
Other dependencies
^^^^^^^^^^^^^^^^^^

View File

@ -10,7 +10,6 @@ import net.corda.core.messaging.CordaRPCOps
import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import net.corda.webserver.services.WebServerPluginRegistry
import java.util.function.Function
import javax.ws.rs.GET
import javax.ws.rs.Path

View File

@ -67,10 +67,21 @@ The bootstrapper tool can be built with the command:
The resulting jar can be found in ``tools/bootstrapper/build/libs/``.
To use it, run the following command, specifying the root directory which hosts all the node directories as the argument:
To use it, create a directory containing a ``node.conf`` file for each node you want to create. Then run the following command:
``java -jar network-bootstrapper.jar <nodes-root-dir>``
For example running the command on a directory containing these files :
.. sourcecode:: none
.
├── notary.conf // The notary's node.conf file
├── partya.conf // Party A's node.conf file
└── partyb.conf // Party B's node.conf file
Would generate directories containing three nodes: notary, partya and partyb.
Starting the nodes
~~~~~~~~~~~~~~~~~~

View File

@ -7,25 +7,32 @@
Shell
=====
The Corda shell is an embedded command line that allows an administrator to control and monitor the node.
Some of its features include:
.. contents::
* Invoking any of the RPCs the node exposes to applications.
* Starting flows.
* View a dashboard of threads, heap usage, VM properties.
* Uploading and downloading zips from the attachment store.
* Issue SQL queries to the underlying database.
* View JMX metrics and monitoring exports.
* UNIX style pipes for both text and objects, an ``egrep`` command and a command for working with columnular data.
The Corda shell is an embedded command line that allows an administrator to control and monitor a node. It is based on
the `CRaSH`_ shell and supports many of the same features. These features include:
It is based on the popular `CRaSH`_ shell used in various other projects and supports many of the same features.
* Invoking any of the node's RPC methods
* Viewing a dashboard of threads, heap usage, VM properties
* Uploading and downloading attachments
* Issuing SQL queries to the underlying database
* Viewing JMX metrics and monitoring exports
* UNIX style pipes for both text and objects, an ``egrep`` command and a command for working with columnular data
Local terminal shell runs only in development mode. It may be disabled by passing the ``--no-local-shell`` flag to the node.
The shell via the local terminal
--------------------------------
SSH server
----------
In development mode, the shell will display in the node's terminal window. It may be disabled by passing the
``--no-local-shell`` flag when running the node.
Shell can also be accessible via SSH. By default SSH server is *disabled*. To enable it port must be configured - in ``node.conf`` file
The shell via SSH
-----------------
The shell is also accessible via SSH.
Enabling SSH access
*******************
By default, the SSH server is *disabled*. To enable it, a port must be configured in the node's ``node.conf`` file:
.. code:: bash
@ -33,80 +40,159 @@ Shell can also be accessible via SSH. By default SSH server is *disabled*. To en
port = 2222
}
Authentication and authorization
--------------------------------
SSH requires users to login first - using the same users as RPC system. In fact, the shell serves as a proxy to RPC and communicates
with the node using RPC calls. This also means that RPC permissions are enforced. No permissions are required to allow the connection
and log in.
Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed`` while starting flows requires
``InvokeRpc.startTrackedFlowDynamic`` and ``InvokeRpc.registeredFlows`` in addition to a permission for a particular flow.
Authentication
**************
Users log in to shell via SSH using the same credentials as for RPC. This is because the shell actually communicates
with the node using RPC calls. No RPC permissions are required to allow the connection and log in.
Host key
--------
The host key is loaded from the ``<node root directory>/sshkey/hostkey.pem`` file. If this file does not exist, it is
generated automatically. In development mode, the seed may be specified to give the same results on the same computer
in order to avoid host-checking errors.
The host key is loaded from ``sshkey/hostkey.pem`` file. If the file does not exist, it will be generated randomly, however
in the development mode seed may be tuned to give the same results on the same computer - in order to avoid host checking
errors.
Connecting to the shell
***********************
Connecting
----------
Linux and MacOS
^^^^^^^^^^^^^^^
Linux and MacOS computers usually come with SSH client preinstalled. On Windows it usually requires extra download.
Usual connection syntax is ``ssh user@host -p 2222`` - where ``user`` is a RPC username, and ``-p`` specifies a port parameters -
it's the same as setup in ``node.conf`` file. ``host`` should point to a node hostname, usually ``localhost`` if connecting and
running node on the same computer. Password will be asked after establishing connection.
Run the following command from the terminal:
:note: While developing, checking multiple samples or simply restarting a node frequently host key may be regenerated. SSH usually
saved once trusted hosts and will refuse to connect in case of a change. Then check may be disabled with extra options
``ssh -o StrictHostKeyChecking=no user@host -p2222``. This option should never be used in production environment!
.. code:: bash
Getting help
------------
ssh -p [portNumber] [host] -l [user]
You can run ``help`` to list the available commands.
Where:
The shell has a ``man`` command that can be used to get interactive help on many commands. You can also use the
``--help`` or ``-h`` flags to a command to get info about what switches it supports.
* ``[portNumber]`` is the port number specified in the ``node.conf`` file
* ``[host]`` is the node's host (e.g. ``localhost`` if running the node locally)
* ``[user]`` is the RPC username
Commands may have subcommands, in the same style as ``git``. In that case running the command by itself will
list the supported subcommands.
The RPC password will be requested after a connection is established.
Starting flows and performing remote method calls
-------------------------------------------------
:note: In development mode, restarting a node frequently may cause the host key to be regenerated. SSH usually saves
trusted hosts and will refuse to connect in case of a change. This check can be disabled using the
``-o StrictHostKeyChecking=no`` flag. This option should never be used in production environment!
**Flows** are the way the ledger is changed. If you aren't familiar with them, please review ":doc:`flow-state-machines`"
first. The ``flow list`` command can be used to list the flows understood by the node, ``flow watch`` shows all the flows
currently running on the node with the result (or error) information in a user friendly way, ``flow start`` can be
used to start flows. The ``flow start`` command takes the class name of a flow, or *any unambiguous substring* and
then the data to be passed to the flow constructor. The unambiguous substring feature is helpful for reducing
the needed typing. If the match is ambiguous the possible matches will be printed out. If a flow has multiple
constructors then the names and types of the arguments will be used to try and determine which to use automatically.
If the match against available constructors is unclear, the reasons each available constructor failed to match
will be printed out. In the case of an ambiguous match, the first applicable will be used.
Windows
^^^^^^^
**RPCs** (remote procedure calls) are commands that can be sent to the node to query it, control it and manage it.
RPCs don't typically do anything that changes the global ledger, but they may change node-specific data in the
database. Each RPC is one method on the ``CordaRPCOps`` interface, and may return a stream of events that will
be shown on screen until you press Ctrl-C. You perform an RPC by using ``run`` followed by the name.
Windows does not provide a built-in SSH tool. An alternative such as PuTTY should be used.
.. raw:: html
Permissions
***********
<center><b><a href="api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html">Documentation of available RPCs</a></b><p></center>
When accessing the shell via SSH, some additional RPC permissions are required:
Whichever form of change is used, there is a need to provide *parameters* to either the RPC or the flow
constructor. Because parameters can be any arbitrary Java object graph, we need a convenient syntax to express
this sort of data. The shell uses a syntax called `Yaml`_ to do this.
* Watching flows (``flow watch``) requires ``InvokeRpc.stateMachinesFeed``
* Starting flows requires ``InvokeRpc.startTrackedFlowDynamic`` and ``InvokeRpc.registeredFlows``, as well as a
permission for the flow being started
Data syntax
-----------
Interacting with the node via the shell
---------------------------------------
Yaml (yet another markup language) is a simple JSON-like way to describe object graphs. It has several features
that make it helpful for our use case, like a lightweight syntax and support for "bare words" which mean you can
often skip the quotes around strings. Here is an example of how this syntax is used:
The shell interacts with the node by issuing RPCs (remote procedure calls). You make an RPC from the shell by typing
``run`` followed by the name of the desired RPC method. For example, you'd see a list of the registered flows on your
node by running:
``run registeredFlows``
Some RPCs return a stream of events that will be shown on screen until you press Ctrl-C.
You can find a list of the available RPC methods
`here <https://docs.corda.net/api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html>`_.
Flow commands
*************
The shell also has special commands for working with flows:
* ``flow list`` lists the flows available on the node
* ``flow watch`` shows all the flows currently running on the node with result (or error) information
* ``flow start`` starts a flow. The ``flow start`` command takes the name of a flow class, or
*any unambiguous substring* thereof, as well as the data to be passed to the flow constructor. If there are several
matches for a given substring, the possible matches will be printed out. If a flow has multiple constructors then the
names and types of the arguments will be used to try and automatically determine which one to use. If the match
against available constructors is unclear, the reasons each available constructor failed to match will be printed
out. In the case of an ambiguous match, the first applicable constructor will be used
Parameter syntax
****************
Parameters are passed to RPC or flow commands using a syntax called `Yaml`_ (yet another markup language), a
simple JSON-like language. The key features of Yaml are:
* Parameters are separated by commas
* Each parameter is specified as a ``key: value`` pair
* There **MUST** to be a space after the colon, otherwise you'll get a syntax error
* Strings do not need to be surrounded by quotes unless they contain commas, colons or embedded quotes
* Class names must be fully-qualified (e.g. ``java.lang.String``)
.. note:: If your CorDapp is written in Java, named arguments won't work unless you compiled the node using the
``-parameters`` argument to javac. See :doc:`generating-a-node` for how to specify it via Gradle.
Creating an instance of a class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Class instances are created using curly-bracket syntax. For example, if we have a ``Campaign`` class with the following
constructor:
``data class Campaign(val name: String, val target: Int)``
Then we could create an instance of this class to pass as a parameter as follows:
``newCampaign: { name: Roger, target: 1000 }``
Where ``newCampaign`` is a parameter of type ``Campaign``.
Mappings from strings to types
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Several parameter types can automatically be mapped from strings. See the `defined parsers`_ for more information. We
cover the most common types here.
Amount
~~~~~~
A parameter of type ``Amount<Currency>`` can be written as either:
* A dollar ($), pound (£) or euro (€) symbol followed by the amount as a decimal
* The amount as a decimal followed by the ISO currency code (e.g. "100.12 CHF")
OpaqueBytes
~~~~~~~~~~~
A parameter of type ``OpaqueBytes`` can be provided as a string, which will be automatically converted to
``OpaqueBytes``.
Party
~~~~~
A parameter of type ``Party`` can be written in several ways:
* By using the node's full name: ``"O=Monogram Bank,L=Sao Paulo,C=GB"``
* By specifying the organisation name only: ``"Monogram Bank"``
* By specifying any other non-ambiguous part of the name: ``"Sao Paulo"`` (if only one network node is located in Sao
Paulo)
Instant
~~~~~~~
A parameter of type ``Instant`` can be written as follows: ``"2017-12-22T00:00:00Z"``.
Examples
^^^^^^^^
Starting a flow
~~~~~~~~~~~~~~~
We would start the ``CashIssue`` flow as follows:
``flow start CashIssue amount: $1000, issueRef: 1234, recipient: "O=Bank A,L=London,C=GB", notary: "O=Notary Service,OU=corda,L=London,C=GB"``
This invokes a constructor of a flow with the following prototype in the code:
This breaks down as follows:
* ``flow start`` is a shell command for starting a flow
* ``CashIssue`` is the flow we want to start
* Each ``name: value`` pair after that is a flow constructor argument
This command invokes the following ``CashIssue`` constructor:
.. container:: codeset
@ -117,50 +203,44 @@ This invokes a constructor of a flow with the following prototype in the code:
val recipient: Party,
val notary: Party) : AbstractCashFlow(progressTracker)
Here, everything after ``CashIssue`` is specifying the arguments to the constructor of a flow. In Yaml, an object
is specified as a set of ``key: value`` pairs and in our form, we separate them by commas. There are a few things
to note about this syntax:
Querying the vault
~~~~~~~~~~~~~~~~~~
* When a parameter is of type ``Amount<Currency>`` you can write it as either one of the dollar symbol ($),
pound (£), euro (€) followed by the amount as a decimal, or as the value followed by the ISO currency code
e.g. "100.12 CHF"
* ``OpaqueBytes`` is filled with the contents of whatever is provided as a string.
* ``Party`` objects are looked up by name.
* Strings do not need to be surrounded by quotes unless they contain a comma or embedded quotes. This makes it
a lot more convenient to type such strings.
We would query the vault for ``IOUState`` states as follows:
Other types also have sensible mappings from strings. See `the defined parsers`_ for more information.
``run vaultQuery contractStateType: com.template.IOUState``
Nested objects can be created using curly braces, as in ``{ a: 1, b: 2}``. This is helpful when no particular
parser is defined for the type you need, for instance, if an API requires a ``Pair<String, Int>``
which could be represented as ``{ first: foo, second: 123 }``.
This breaks down as follows:
.. note:: If your CorDapp is written in Java,
named arguments won't work unless you compiled using the ``-parameters`` argument to javac.
See :doc:`generating-a-node` for how to specify it via Gradle.
The same syntax is also used to specify the parameters for RPCs, accessed via the ``run`` command, like this:
``run registeredFlows``
* ``run`` is a shell command for making an RPC call
* ``vaultQuery`` is the RPC call we want to make
* ``contractStateType: com.template.IOUState`` is the fully-qualified name of the state type we are querying for
Attachments
-----------
***********
The shell can be used to upload and download attachments from the node interactively. To learn more, see
the tutorial ":doc:`tutorial-attachments`".
The shell can be used to upload and download attachments from the node. To learn more, see the tutorial
":doc:`tutorial-attachments`".
Getting help
************
You can type ``help`` in the shell to list the available commands, and ``man`` to get interactive help on many
commands. You can also pass the ``--help`` or ``-h`` flags to a command to get info about what switches it supports.
Commands may have subcommands, in the same style as ``git``. In that case, running the command by itself will
list the supported subcommands.
Extending the shell
-------------------
The shell can be extended using commands written in either Java or `Groovy`_ (Groovy is a scripting language that
is Java compatible). Such commands have full access to the node internal APIs and thus can be used to achieve
almost anything.
The shell can be extended using commands written in either Java or `Groovy`_ (a Java-compatible scripting language).
These commands have full access to the node's internal APIs and thus can be used to achieve almost anything.
A full tutorial on how to write such commands is out of scope for this documentation, to learn more please
refer to the `CRaSH`_ documentation. New commands can be placed in the ``shell-commands`` subdirectory in the
node directory. Edits to existing commands will be used automatically, but at this time commands added after the
node has started won't be automatically detected. Commands should be named in all lower case with either a
``.java`` or ``.groovy`` extension.
A full tutorial on how to write such commands is out of scope for this documentation. To learn more, please refer to
the `CRaSH`_ documentation. New commands are placed in the ``shell-commands`` subdirectory in the node directory. Edits
to existing commands will be used automatically, but currently commands added after the node has started won't be
automatically detected. Commands must have names all in lower-case with either a ``.java`` or ``.groovy`` extension.
.. warning:: Commands written in Groovy ignore Java security checks, so have unrestricted access to node and JVM
internals regardless of any sandboxing that may be in place. Don't allow untrusted users to edit files in the
@ -171,14 +251,13 @@ Limitations
The shell will be enhanced over time. The currently known limitations include:
* SSH access is currently not available.
* There is no command completion for flows or RPCs.
* Command history is not preserved across restarts.
* The ``jdbc`` command requires you to explicitly log into the database first.
* Commands placed in the ``shell-commands`` directory are only noticed after the node is restarted.
* The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using.
* There is no command completion for flows or RPCs
* Command history is not preserved across restarts
* The ``jdbc`` command requires you to explicitly log into the database first
* Commands placed in the ``shell-commands`` directory are only noticed after the node is restarted
* The ``jul`` command advertises access to logs, but it doesn't work with the logging framework we're using
.. _Yaml: http://www.yaml.org/spec/1.2/spec.html
.. _the defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html
.. _defined parsers: api/kotlin/corda/net.corda.client.jackson/-jackson-support/index.html
.. _Groovy: http://groovy-lang.org/
.. _CRaSH: http://www.crashub.org/

View File

@ -35,8 +35,14 @@ the transaction to the regulator. There are two important aspects to note here:
If the states define a relational mapping (see :doc:`api-persistence`) then the regulator will be able to query the
reports from their database and observe new transactions coming in via RPC.
.. warning:: Nodes which act as both observers and which directly take part in the ledger are not supported at this
time. In particular, coin selection may return states which you do not have the private keys to be able to sign
for. Future versions of Corda may address this issue, but for now, if you wish to both participate in the ledger
and also observe transactions that you can't sign for you will need to run two nodes and have two separate
identities.
Caveats
-------
* Nodes which act as both observers and direct participants in the ledger are not supported at this time. In
particular, coin selection may return states which you do not have the private keys to be able to sign for. Future
versions of Corda may address this issue, but for now, if you wish to both participate in the ledger and also observe
transactions that you can't sign for you will need to run two nodes and have two separate identities
* Nodes only record each transaction once. If a node has already recorded a transaction in non-observer mode, it cannot
later re-record the same transaction as an observer. This issue is tracked here:
https://r3-cev.atlassian.net/browse/CORDA-883

View File

@ -26,7 +26,7 @@ additional classes are used when the jar is invoked directly. To do this we'll u
./gradlew experimental:quasar-hook:jar
./gradlew samples:irs-demo:deployNodes
cd samples/irs-demo/build/nodes/NotaryService
java -javaagent:../../../../../experimental/quasar-hook/build/libs/quasar-hook.jar=expand=com,de,org,co,io;truncate=net.corda;alwaysExcluded=com.opengamma,io.atomix -jar corda.jar
java -javaagent:../../../../../experimental/quasar-hook/build/libs/quasar-hook.jar=expand=com,de,org,co,io;truncate=net.corda;alwaysExcluded=com.opengamma,io.atomix,org.jolokia -jar corda.jar
```
Once the node is started just exit the node.

View File

@ -90,8 +90,12 @@ private abstract class JavaCommand(
add(getJavaPath())
addAll(jvmArgs)
add("-Dname=$nodeName")
null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
null != monitoringPort && add("-Dcapsule.jvm.args=-javaagent:drivers/$jolokiaJar=port=$monitoringPort")
val jvmArgs: MutableList<String> = mutableListOf()
null != debugPort && jvmArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
null != monitoringPort && jvmArgs.add("-javaagent:drivers/$jolokiaJar=port=$monitoringPort")
if (jvmArgs.isNotEmpty()) {
add("-Dcapsule.jvm.args=${jvmArgs.joinToString(separator = " ")}")
}
add("-jar")
add(jarName)
init()

View File

@ -0,0 +1,80 @@
package net.corda.nodeapi.internal
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.cert
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.toX509CertHolder
import net.corda.core.utilities.trace
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
import net.corda.nodeapi.internal.crypto.*
import org.slf4j.LoggerFactory
import java.nio.file.Path
/**
* Contains utility methods for generating identities for a node.
*
* WARNING: This is not application for production use.
*/
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")
}
// TODO The passwords for the dev key stores are spread everywhere and should be constants in a single location
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
nodeSslConfig.certificatesDirectory.createDirectories()
nodeSslConfig.createDevKeyStores(rootCert.toX509CertHolder(), intermediateCa, legalName)
val keyStoreWrapper = KeyStoreWrapper(nodeSslConfig.nodeKeystore, nodeSslConfig.keyStorePassword)
val identity = keyStoreWrapper.storeLegalIdentity(legalName, "$NODE_IDENTITY_ALIAS_PREFIX-private-key", Crypto.generateKeyPair())
return identity.party
}
fun generateDistributedNotaryIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
require(dirs.isNotEmpty())
log.trace { "Generating identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
val keyPairs = (1..dirs.size).map { generateKeyPair() }
val compositeKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
keyPairs.zip(dirs) { keyPair, nodeDir ->
val (serviceKeyCert, compositeKeyCert) = listOf(keyPair.public, compositeKey).map { publicKey ->
X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, notaryName, publicKey)
}
val distServKeyStoreFile = (nodeDir / "certificates").createDirectories() / "distributedService.jks"
val keystore = loadOrCreateKeyStore(distServKeyStoreFile, "cordacadevpass")
keystore.setCertificateEntry("$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key", compositeKeyCert.cert)
keystore.setKeyEntry(
"$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key",
keyPair.private,
"cordacadevkeypass".toCharArray(),
arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert))
keystore.save(distServKeyStoreFile, "cordacadevpass")
}
return Party(notaryName, compositeKey)
}
}

View File

@ -1,66 +0,0 @@
package net.corda.nodeapi.internal
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.cert
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.utilities.trace
import net.corda.nodeapi.internal.crypto.*
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.security.cert.X509Certificate
object IdentityGenerator {
private val log = LoggerFactory.getLogger(javaClass)
const val NODE_IDENTITY_ALIAS_PREFIX = "identity"
const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary"
fun generateNodeIdentity(dir: Path, legalName: CordaX500Name, customRootCert: X509Certificate? = null): Party {
return generateToDisk(listOf(dir), legalName, NODE_IDENTITY_ALIAS_PREFIX, threshold = 1, customRootCert = customRootCert)
}
fun generateDistributedNotaryIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1, customRootCert: X509Certificate? = null): Party {
return generateToDisk(dirs, notaryName, DISTRIBUTED_NOTARY_ALIAS_PREFIX, threshold, customRootCert)
}
/**
* Generates signing key pairs and a common distributed service identity for a set of nodes.
* The key pairs and the group identity get serialized to disk in the corresponding node directories.
* This method should be called *before* any of the nodes are started.
*
* @param dirs List of node directories to place the generated identity and key pairs in.
* @param name The name of the identity.
* @param threshold The threshold for the generated group [CompositeKey].
* @param customRootCert the certificate to use as the Corda root CA. If not specified the one in
* internal/certificates/cordadevcakeys.jks is used.
*/
private fun generateToDisk(dirs: List<Path>,
name: CordaX500Name,
aliasPrefix: String,
threshold: Int,
customRootCert: X509Certificate?): Party {
log.trace { "Generating identity \"$name\" for nodes: ${dirs.joinToString()}" }
val keyPairs = (1..dirs.size).map { generateKeyPair() }
val key = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold)
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
val rootCert = customRootCert ?: caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
keyPairs.zip(dirs) { keyPair, dir ->
val serviceKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, keyPair.public)
val compositeKeyCert = X509Utilities.createCertificate(CertificateType.SERVICE_IDENTITY, intermediateCa.certificate, intermediateCa.keyPair, name, key)
val certPath = (dir / "certificates").createDirectories() / "distributedService.jks"
val keystore = loadOrCreateKeyStore(certPath, "cordacadevpass")
keystore.setCertificateEntry("$aliasPrefix-composite-key", compositeKeyCert.cert)
keystore.setKeyEntry("$aliasPrefix-private-key", keyPair.private, "cordacadevkeypass".toCharArray(), arrayOf(serviceKeyCert.cert, intermediateCa.certificate.cert, rootCert))
keystore.save(certPath, "cordacadevpass")
}
return Party(name, key)
}
}

View File

@ -0,0 +1,57 @@
package net.corda.nodeapi.internal
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.x500Name
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.crypto.*
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.cert.X509CertificateHolder
/**
* 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(rootCert: X509CertificateHolder, intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name) {
val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediateCa, legalName)
loadOrCreateKeyStore(nodeKeystore, keyStorePassword).apply {
addOrReplaceKey(
X509Utilities.CORDA_CLIENT_CA,
nodeCaKeyPair.private,
keyStorePassword.toCharArray(),
arrayOf(nodeCaCert, intermediateCa.certificate, rootCert))
save(nodeKeystore, keyStorePassword)
}
val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName, tlsKeyPair.public)
loadOrCreateKeyStore(sslKeystore, keyStorePassword).apply {
addOrReplaceKey(
X509Utilities.CORDA_CLIENT_TLS,
tlsKeyPair.private,
keyStorePassword.toCharArray(),
arrayOf(tlsCert, nodeCaCert, intermediateCa.certificate, rootCert))
save(sslKeystore, keyStorePassword)
}
}
/**
* Create a dev node CA cert, as a sub-cert of the given [intermediateCa], and matching key pair using the given
* [CordaX500Name] as the cert subject.
*/
fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name): CertificateAndKeyPair {
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
val cert = X509Utilities.createCertificate(
CertificateType.NODE_CA,
intermediateCa.certificate,
intermediateCa.keyPair,
legalName,
keyPair.public,
nameConstraints = nameConstraints)
return CertificateAndKeyPair(cert, keyPair)
}

View File

@ -8,6 +8,7 @@ interface SSLConfiguration {
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"
}

View File

@ -1,41 +1,26 @@
package net.corda.nodeapi.internal.crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.cert
import net.corda.core.internal.read
import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.Certificate
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
private fun createCertificate(serviceName: CordaX500Name, pubKey: PublicKey): CertPath {
val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
// TODO This method seems misplaced in this class.
fun storeLegalIdentity(legalName: CordaX500Name, alias: String, keyPair: KeyPair): PartyAndCertificate {
val nodeCaCertChain = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
val nodeCa = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
val identityCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, nodeCa.certificate, nodeCa.keyPair, legalName, keyPair.public)
val identityCertPath = X509CertificateFactory().generateCertPath(identityCert.cert, *nodeCaCertChain)
// Assume key password = store password.
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
// Create new keys and store in keystore.
val cert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
val certPath = X509CertificateFactory().generateCertPath(cert.cert, *clientCertPath)
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
// TODO: X509Utilities.validateCertificateChain()
return certPath
}
fun signAndSaveNewKeyPair(serviceName: CordaX500Name, privateKeyAlias: String, keyPair: KeyPair) {
val certPath = createCertificate(serviceName, keyPair.public)
// Assume key password = store password.
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray())
keyStore.save(storePath, storePassword)
}
fun savePublicKey(serviceName: CordaX500Name, pubKeyAlias: String, pubKey: PublicKey) {
val certPath = createCertificate(serviceName, pubKey)
// Assume key password = store password.
keyStore.addOrReplaceCertificate(pubKeyAlias, certPath.certificates.first())
keyStore.addOrReplaceKey(alias, keyPair.private, storePassword.toCharArray(), identityCertPath.certificates.toTypedArray())
keyStore.save(storePath, storePassword)
return PartyAndCertificate(identityCertPath)
}
// Delegate methods to keystore. Sadly keystore doesn't have an interface.
@ -47,5 +32,5 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St
fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias)
fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
fun getCertificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
}

View File

@ -4,12 +4,8 @@ import net.corda.core.CordaOID
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.CertRole
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.cert
import net.corda.core.internal.reader
import net.corda.core.internal.writer
import net.corda.core.internal.x500Name
import net.corda.core.internal.*
import net.corda.core.utilities.days
import net.corda.core.utilities.millis
import org.bouncycastle.asn1.*
@ -43,6 +39,7 @@ 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
// Aliases for private keys and certificates.
const val CORDA_ROOT_CA = "cordarootca"
const val CORDA_INTERMEDIATE_CA = "cordaintermediateca"

View File

@ -4,23 +4,28 @@ import com.typesafe.config.ConfigFactory
import net.corda.cordform.CordformNode
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.fork
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal._contextSerializationEnv
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.time.Instant
import java.util.concurrent.TimeUnit.SECONDS
import java.util.concurrent.Executors
import java.util.concurrent.TimeoutException
import kotlin.streams.toList
/**
@ -48,13 +53,14 @@ class NetworkBootstrapper {
fun bootstrap(directory: Path) {
directory.createDirectories()
println("Bootstrapping local network in $directory")
generateDirectoriesIfNeeded(directory)
val nodeDirs = directory.list { paths -> paths.filter { (it / "corda.jar").exists() }.toList() }
require(nodeDirs.isNotEmpty()) { "No nodes found" }
println("Nodes found in the following sub-directories: ${nodeDirs.map { it.fileName }}")
val processes = startNodeInfoGeneration(nodeDirs)
initialiseSerialization()
try {
println("Waiting for all nodes to generate their node-info files")
println("Waiting for all nodes to generate their node-info files...")
val nodeInfoFiles = gatherNodeInfoFiles(processes, nodeDirs)
println("Distributing all node info-files to all nodes")
distributeNodeInfos(nodeDirs, nodeInfoFiles)
@ -69,6 +75,27 @@ class NetworkBootstrapper {
}
}
private fun generateDirectoriesIfNeeded(directory: Path) {
val confFiles = directory.list { it.filter { it.toString().endsWith(".conf") }.toList() }
if (confFiles.isEmpty()) return
println("Node config files found in the root directory - generating node directories")
val cordaJar = extractCordaJarTo(directory)
for (confFile in confFiles) {
val nodeName = confFile.fileName.toString().removeSuffix(".conf")
println("Generating directory for $nodeName")
val nodeDir = (directory / nodeName).createDirectory()
confFile.moveTo(nodeDir / "node.conf")
Files.copy(cordaJar, (nodeDir / "corda.jar"))
}
Files.delete(cordaJar)
}
private fun extractCordaJarTo(directory: Path): Path {
val cordaJarPath = (directory / "corda.jar")
Thread.currentThread().contextClassLoader.getResourceAsStream("corda.jar").copyTo(cordaJarPath)
return cordaJarPath
}
private fun startNodeInfoGeneration(nodeDirs: List<Path>): List<Process> {
return nodeDirs.map { nodeDir ->
val logsDir = (nodeDir / LOGS_DIR_NAME).createDirectories()
@ -82,18 +109,25 @@ class NetworkBootstrapper {
}
private fun gatherNodeInfoFiles(processes: List<Process>, nodeDirs: List<Path>): List<Path> {
val timeOutInSeconds = 60L
return processes.zip(nodeDirs).map { (process, nodeDir) ->
check(process.waitFor(timeOutInSeconds, SECONDS)) {
"Node in ${nodeDir.fileName} took longer than ${timeOutInSeconds}s to generate its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
}
check(process.exitValue() == 0) {
val executor = Executors.newSingleThreadExecutor()
val future = executor.fork {
processes.zip(nodeDirs).map { (process, nodeDir) ->
check(process.waitFor() == 0) {
"Node in ${nodeDir.fileName} exited with ${process.exitValue()} when generating its node-info - see logs in ${nodeDir / LOGS_DIR_NAME}"
}
nodeDir.list { paths -> paths.filter { it.fileName.toString().startsWith("nodeInfo-") }.findFirst().get() }
}
}
return try {
future.getOrThrow(60.seconds)
} catch (e: TimeoutException) {
println("...still waiting. If this is taking longer than usual, check the node logs.")
future.getOrThrow()
}
}
private fun distributeNodeInfos(nodeDirs: List<Path>, nodeInfoFiles: List<Path>) {
for (nodeDir in nodeDirs) {
val additionalNodeInfosDir = (nodeDir / CordformNode.NODE_INFO_DIRECTORY).createDirectories()
@ -161,6 +195,7 @@ class NetworkBootstrapper {
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P
}
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
}

View File

@ -12,12 +12,17 @@ import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.services.config.createKeystoreForCordaNode
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.createDevKeyStores
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import net.corda.testing.*
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
import net.corda.testing.TestIdentity
import net.corda.testing.internal.createDevIntermediateCaCertPath
import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.BasicConstraints
import org.bouncycastle.asn1.x509.Extension
@ -33,11 +38,8 @@ import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.nio.file.Path
import java.security.KeyStore
import java.security.PrivateKey
import java.security.SecureRandom
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.util.*
import java.util.stream.Stream
@ -52,6 +54,11 @@ class X509UtilitiesTest {
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
val BOB get() = bob.party
val BOB_PUBKEY get() = bob.publicKey
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"
)
}
@Rule
@ -156,106 +163,61 @@ class X509UtilitiesTest {
}
@Test
fun `create full CA keystore`() {
val tmpKeyStore = tempFile("keystore.jks")
val tmpTrustStore = tempFile("truststore.jks")
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
createCAKeyStoreAndTrustStore(tmpKeyStore, "keystorepass", "keypass", tmpTrustStore, "trustpass")
// Load back generated root CA Cert and private key from keystore and check against copy in truststore
val keyStore = loadKeyStore(tmpKeyStore, "keystorepass")
val trustStore = loadKeyStore(tmpTrustStore, "trustpass")
val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA, "keypass".toCharArray()) as PrivateKey
val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
assertEquals(rootCaCert, rootCaFromTrustStore)
rootCaCert.checkValidity(Date())
rootCaCert.verify(rootCaCert.publicKey)
// Now sign something with private key and verify against certificate public key
val testData = "12345".toByteArray()
val caSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData)
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) }
// Load back generated intermediate CA Cert and private key
val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA) as X509Certificate
val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA, "keypass".toCharArray()) as PrivateKey
intermediateCaCert.checkValidity(Date())
intermediateCaCert.verify(rootCaCert.publicKey)
// Now sign something with private key and verify against certificate public key
val intermediateSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData)
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) }
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"
}
@Test
fun `create server certificate in keystore for SSL`() {
val tmpCAKeyStore = tempFile("keystore.jks")
val tmpTrustStore = tempFile("truststore.jks")
val tmpSSLKeyStore = tempFile("sslkeystore.jks")
val tmpServerKeyStore = tempFile("serverkeystore.jks")
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
createCAKeyStoreAndTrustStore(tmpCAKeyStore,
"cakeystorepass",
"cakeypass",
tmpTrustStore,
"trustpass")
// Load signing intermediate CA cert
val caKeyStore = loadKeyStore(tmpCAKeyStore, "cakeystorepass")
val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cakeypass")
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
// Generate server cert and private key and populate another keystore suitable for SSL
createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name)
sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name)
// Load back server certificate
val serverKeyStore = loadKeyStore(tmpServerKeyStore, "serverstorepass")
val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, "serverkeypass")
val serverKeyStore = loadKeyStore(sslConfig.nodeKeystore, sslConfig.keyStorePassword)
val (serverCert, serverKeyPair) = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, sslConfig.keyStorePassword)
serverCertAndKey.certificate.isValidOn(Date())
serverCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(caCertAndKey.certificate.subjectPublicKeyInfo))
serverCert.cert.checkValidity()
serverCert.cert.verify(intermediateCa.certificate.cert.publicKey)
assertThat(CordaX500Name.parse(serverCert.subject.toString())).isEqualTo(MEGA_CORP.name)
assertTrue { serverCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) }
// Load back SSL certificate
val sslKeyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
val (sslCert) = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslConfig.keyStorePassword)
// Load back server certificate
val sslKeyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass")
val sslCertAndKey = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, "serverkeypass")
sslCert.cert.checkValidity()
sslCert.cert.verify(serverCert.cert.publicKey)
assertThat(CordaX500Name.parse(sslCert.subject.toString())).isEqualTo(MEGA_CORP.name)
sslCertAndKey.certificate.isValidOn(Date())
sslCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(serverCertAndKey.certificate.subjectPublicKeyInfo))
assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) }
// Now sign something with private key and verify against certificate public key
val testData = "123456".toByteArray()
val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData)
val publicKey = Crypto.toSupportedPublicKey(serverCertAndKey.certificate.subjectPublicKeyInfo)
val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverKeyPair.private, testData)
val publicKey = Crypto.toSupportedPublicKey(serverCert.subjectPublicKeyInfo)
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) }
}
@Test
fun `create server cert and use in SSL socket`() {
val tmpCAKeyStore = tempFile("keystore.jks")
val tmpTrustStore = tempFile("truststore.jks")
val tmpSSLKeyStore = tempFile("sslkeystore.jks")
val tmpServerKeyStore = tempFile("serverkeystore.jks")
val sslConfig = object : SSLConfiguration {
override val certificatesDirectory = tempFolder.root.toPath()
override val keyStorePassword = "serverstorepass"
override val trustStorePassword = "trustpass"
}
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
val caKeyStore = createCAKeyStoreAndTrustStore(tmpCAKeyStore,
"cakeystorepass",
"cakeypass",
tmpTrustStore,
"trustpass")
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
// Generate server cert and private key and populate another keystore suitable for SSL
createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", MEGA_CORP.name)
val keyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass")
val trustStore = loadKeyStore(tmpTrustStore, "trustpass")
sslConfig.createDevKeyStores(rootCa.certificate, intermediateCa, MEGA_CORP.name)
sslConfig.createTrustStore(rootCa.certificate.cert)
val keyStore = loadKeyStore(sslConfig.sslKeystore, sslConfig.keyStorePassword)
val trustStore = loadKeyStore(sslConfig.trustStoreFile, sslConfig.trustStorePassword)
val context = SSLContext.getInstance("TLS")
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, "serverstorepass".toCharArray())
keyManagerFactory.init(keyStore, sslConfig.keyStorePassword.toCharArray())
val keyManagers = keyManagerFactory.keyManagers
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustMgrFactory.init(trustStore)
@ -266,13 +228,7 @@ class X509UtilitiesTest {
val clientSocketFactory = context.socketFactory
val serverSocket = serverSocketFactory.createServerSocket(0) as SSLServerSocket // use 0 to get first free socket
val serverParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"),
arrayOf("TLSv1.2"))
val serverParams = SSLParameters(CIPHER_SUITES, arrayOf("TLSv1.2"))
serverParams.wantClientAuth = true
serverParams.needClientAuth = true
serverParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
@ -280,13 +236,7 @@ class X509UtilitiesTest {
serverSocket.useClientMode = false
val clientSocket = clientSocketFactory.createSocket() as SSLSocket
val clientParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"),
arrayOf("TLSv1.2"))
val clientParams = SSLParameters(CIPHER_SUITES, arrayOf("TLSv1.2"))
clientParams.endpointIdentificationAlgorithm = null // Reconfirm default no server name indication, use our own validator.
clientSocket.sslParameters = clientParams
clientSocket.useClientMode = true
@ -344,60 +294,14 @@ class X509UtilitiesTest {
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
/**
* All in one wrapper to manufacture a root CA cert and an Intermediate CA cert.
* Normally this would be run once and then the outputs would be re-used repeatedly to manufacture the server certs
* @param keyStoreFilePath The output KeyStore path to publish the private keys of the CA root and intermediate certs into.
* @param storePassword The storage password to protect access to the generated KeyStore and public certificates
* @param keyPassword The password that protects the CA private keys.
* Unlike the SSL libraries that tend to assume the password is the same as the keystore password.
* These CA private keys should be protected more effectively with a distinct password.
* @param trustStoreFilePath The output KeyStore to place the Root CA public certificate, which can be used as an SSL truststore
* @param trustStorePassword The password to protect the truststore
* @return The KeyStore object that was saved to file
*/
private fun createCAKeyStoreAndTrustStore(keyStoreFilePath: Path,
storePassword: String,
keyPassword: String,
trustStoreFilePath: Path,
trustStorePassword: String
): KeyStore {
val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val baseName = CordaX500Name(organisation = "R3CEV", locality = "London", country = "GB")
val rootCACert = X509Utilities.createSelfSignedCACertificate(baseName.copy(commonName = "Corda Node Root CA"), rootCAKey)
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(
CertificateType.INTERMEDIATE_CA,
rootCACert,
rootCAKey,
baseName.copy(commonName = "Corda Node Intermediate CA"),
intermediateCAKeyPair.public)
val keyPass = keyPassword.toCharArray()
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, rootCAKey.private, keyPass, arrayOf<Certificate>(rootCACert.cert))
keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA,
intermediateCAKeyPair.private,
keyPass,
Stream.of(intermediateCACert, rootCACert).map { it.cert }.toTypedArray<Certificate>())
keyStore.save(keyStoreFilePath, storePassword)
val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword)
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert)
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCACert.cert)
trustStore.save(trustStoreFilePath, trustStorePassword)
return keyStore
private fun SSLConfiguration.createTrustStore(rootCert: X509Certificate) {
val trustStore = loadOrCreateKeyStore(trustStoreFile, trustStorePassword)
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
trustStore.save(trustStoreFile, trustStorePassword)
}
@Test
fun `Get correct private key type from Keystore`() {
fun `get correct private key type from Keystore`() {
val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
val testName = CordaX500Name(commonName = "Test", organisation = "R3 Ltd", locality = "London", country = "GB")
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair)

View File

@ -44,7 +44,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
applicationVersion = corda_release_version
appClassPath = ["jolokia-war-${project.rootProject.ext.jolokia_version}.war"]
// See experimental/quasar-hook/README.md for how to generate.
def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)"
def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)"
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
systemProperties['visualvm.display.name'] = 'CordaEnterprise'
minJavaVersion = '1.8.0'

View File

@ -14,11 +14,9 @@ import net.corda.testing.IntegrationTestSchemas
import net.corda.testing.driver.driver
import net.corda.testing.toDatabaseSchemaName
import org.junit.ClassRule
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import java.nio.file.Path
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class NodeKeystoreCheckTest : IntegrationTest() {
companion object {
@ -27,15 +25,17 @@ class NodeKeystoreCheckTest : IntegrationTest() {
}
@Test
fun `node should throw exception if cert path doesn't chain to the trust root`() {
fun `starting node in non-dev mode with no key store`() {
driver(startNodesInProcess = true) {
// This will fail because there are no keystore configured.
assertFailsWith(IllegalArgumentException::class) {
assertThatThrownBy {
startNode(customOverrides = mapOf("devMode" to false)).getOrThrow()
}.apply {
assertTrue(message?.startsWith("Identity certificate not found. ") ?: false)
}.hasMessageContaining("Identity certificate not found")
}
}
@Test
fun `node should throw exception if cert path doesn't chain to the trust root`() {
driver(startNodesInProcess = true) {
// Create keystores
val keystorePassword = "password"
val config = object : SSLConfiguration {
@ -46,9 +46,12 @@ class NodeKeystoreCheckTest : IntegrationTest() {
config.configureDevKeyAndTrustStores(ALICE_NAME)
// This should pass with correct keystore.
val node = startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false,
val node = startNode(
providedName = ALICE_NAME,
customOverrides = mapOf("devMode" to false,
"keyStorePassword" to keystorePassword,
"trustStorePassword" to keystorePassword)).get()
"trustStorePassword" to keystorePassword)
).getOrThrow()
node.stop()
// Fiddle with node keystore.
@ -62,11 +65,9 @@ class NodeKeystoreCheckTest : IntegrationTest() {
keystore.setKeyEntry(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, config.keyStorePassword.toCharArray(), arrayOf(badNodeCACert.cert, badRoot.cert))
keystore.save(config.nodeKeystore, config.keyStorePassword)
assertFailsWith(IllegalArgumentException::class) {
assertThatThrownBy {
startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow()
}.apply {
assertEquals("Client CA certificate must chain to the trusted root.", message)
}
}.hasMessage("Client CA certificate must chain to the trusted root.")
}
}
}

View File

@ -23,7 +23,7 @@ import net.corda.node.services.config.BFTSMaRtConfiguration
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.transactions.minClusterSize
import net.corda.node.services.transactions.minCorrectReplicas
import net.corda.nodeapi.internal.IdentityGenerator
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.testing.IntegrationTest
@ -69,7 +69,7 @@ class BFTNotaryServiceTests : IntegrationTest() {
(Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists?
val replicaIds = (0 until clusterSize)
notary = IdentityGenerator.generateDistributedNotaryIdentity(
notary = DevIdentityGenerator.generateDistributedNotaryIdentity(
replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) },
CordaX500Name("BFT", "Zurich", "CH"))

View File

@ -1,6 +1,11 @@
package net.corda.node.services.network
import net.corda.cordform.CordformNode
import net.corda.core.crypto.SignedData
import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.concurrent.transpose
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.internal.list
import net.corda.core.internal.readAll
import net.corda.core.node.NodeInfo
@ -10,6 +15,10 @@ import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.testing.*
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation
import net.corda.testing.node.internal.CompatibilityZoneParams
@ -18,6 +27,8 @@ import net.corda.testing.node.internal.network.NetworkMapServer
import org.assertj.core.api.Assertions.assertThat
import org.junit.*
import java.net.URL
import java.time.Instant
import kotlin.streams.toList
import kotlin.test.assertEquals
class NetworkMapTest : IntegrationTest() {
@ -29,6 +40,7 @@ class NetworkMapTest : IntegrationTest() {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val cacheTimeout = 1.seconds
private val portAllocation = PortAllocation.Incremental(10000)
@ -39,7 +51,9 @@ class NetworkMapTest : IntegrationTest() {
fun start() {
networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort())
val address = networkMapServer.start()
compatibilityZone = CompatibilityZoneParams(URL("http://$address"))
compatibilityZone = CompatibilityZoneParams(URL("http://$address"), publishNotaries = {
networkMapServer.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()))
})
}
@After
@ -49,25 +63,35 @@ class NetworkMapTest : IntegrationTest() {
@Test
fun `node correctly downloads and saves network parameters file on startup`() {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
initialiseSerialization = false,
notarySpecs = emptyList()
) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
val networkParameters = alice.configuration.baseDirectory
.list { paths -> paths.filter { it.fileName.toString() == NETWORK_PARAMS_FILE_NAME }.findFirst().get() }
val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
.readAll()
.deserialize<SignedData<NetworkParameters>>()
.verified()
assertEquals(NetworkMapServer.stubNetworkParameter, networkParameters)
// We use a random modified time above to make the network parameters unqiue so that we're sure they came
// from the server
assertEquals(networkMapServer.networkParameters, networkParameters)
}
}
@Test
fun `nodes can see each other using the http network map`() {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
val alice = startNode(providedName = ALICE_NAME)
val bob = startNode(providedName = BOB_NAME)
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
val bobNode = bob.get()
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
initialiseSerialization = false
) {
val (aliceNode, bobNode, notaryNode) = listOf(
startNode(providedName = ALICE_NAME),
startNode(providedName = BOB_NAME),
defaultNotaryNode
).transpose().getOrThrow()
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
@ -77,15 +101,20 @@ class NetworkMapTest : IntegrationTest() {
@Test
fun `nodes process network map add updates correctly when adding new node to network map`() {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
val alice = startNode(providedName = ALICE_NAME)
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
initialiseSerialization = false
) {
val (aliceNode, notaryNode) = listOf(
startNode(providedName = ALICE_NAME),
defaultNotaryNode
).transpose().getOrThrow()
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
val bob = startNode(providedName = BOB_NAME)
val bobNode = bob.get()
val bobNode = startNode(providedName = BOB_NAME).getOrThrow()
// Wait for network map client to poll for the next update.
Thread.sleep(cacheTimeout.toMillis() * 2)
@ -98,12 +127,16 @@ class NetworkMapTest : IntegrationTest() {
@Test
fun `nodes process network map remove updates correctly`() {
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone, initialiseSerialization = false) {
val alice = startNode(providedName = ALICE_NAME)
val bob = startNode(providedName = BOB_NAME)
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
val bobNode = bob.get()
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
initialiseSerialization = false
) {
val (aliceNode, bobNode, notaryNode) = listOf(
startNode(providedName = ALICE_NAME),
startNode(providedName = BOB_NAME),
defaultNotaryNode
).transpose().getOrThrow()
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
@ -119,5 +152,12 @@ class NetworkMapTest : IntegrationTest() {
}
}
private fun NodeHandle.onlySees(vararg nodes: NodeInfo) = assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
private fun NodeHandle.onlySees(vararg nodes: NodeInfo) {
// Make sure the nodes aren't getting the node infos from their additional directories
val nodeInfosDir = configuration.baseDirectory / CordformNode.NODE_INFO_DIRECTORY
if (nodeInfosDir.exists()) {
assertThat(nodeInfosDir.list { it.toList() }).isEmpty()
}
assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
}
}

View File

@ -3,10 +3,15 @@ package net.corda.node.utilities.registration
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.cert
import net.corda.core.internal.concurrent.transpose
import net.corda.core.internal.toX509CertHolder
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.minutes
import net.corda.finance.DOLLARS
import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
@ -16,11 +21,16 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.testing.IntegrationTest
import net.corda.testing.IntegrationTestSchemas
import net.corda.testing.ROOT_CA
import net.corda.testing.SerializationEnvironmentRule
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.driver.PortAllocation
import net.corda.testing.node.internal.CompatibilityZoneParams
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.internal.CompatibilityZoneParams
import net.corda.testing.node.internal.internalDriver
import net.corda.testing.node.internal.network.NetworkMapServer
import net.corda.testing.singleIdentity
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.pkcs.PKCS10CertificationRequest
@ -31,6 +41,7 @@ import java.io.InputStream
import java.net.URL
import java.security.KeyPair
import java.security.cert.CertPath
import java.security.cert.CertPathValidatorException
import java.security.cert.Certificate
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@ -42,20 +53,25 @@ class NodeRegistrationTest : IntegrationTest() {
companion object {
@ClassRule @JvmField
val databaseSchemas = IntegrationTestSchemas("Alice")
private val notaryName = CordaX500Name("NotaryService", "Zurich", "CH")
private val aliceName = CordaX500Name("Alice", "London", "GB")
private val genevieveName = CordaX500Name("Genevieve", "London", "GB")
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val portAllocation = PortAllocation.Incremental(13000)
private val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate()
private val registrationHandler = RegistrationHandler(rootCertAndKeyPair)
private val registrationHandler = RegistrationHandler(ROOT_CA)
private lateinit var server: NetworkMapServer
private lateinit var serverHostAndPort: NetworkHostAndPort
@Before
fun startServer() {
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), rootCertAndKeyPair, registrationHandler)
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), ROOT_CA, "localhost", registrationHandler)
serverHostAndPort = server.start()
}
@ -64,50 +80,65 @@ class NodeRegistrationTest : IntegrationTest() {
server.close()
}
// TODO Ideally this test should be checking that two nodes that register are able to transact with each other. However
// starting a second node hangs so that needs to be fixed.
@Test
fun `node registration correct root cert`() {
val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = rootCertAndKeyPair.certificate.cert)
val compatibilityZone = CompatibilityZoneParams(
URL("http://$serverHostAndPort"),
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
rootCert = ROOT_CA.certificate.cert)
internalDriver(
portAllocation = portAllocation,
notarySpecs = emptyList(),
compatibilityZone = compatibilityZone,
initialiseSerialization = false
initialiseSerialization = false,
notarySpecs = listOf(NotarySpec(notaryName)),
extraCordappPackagesToScan = listOf("net.corda.finance")
) {
startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow()
assertThat(registrationHandler.idsPolled).contains("Alice")
val nodes = listOf(
startNode(providedName = aliceName),
startNode(providedName = genevieveName),
defaultNotaryNode
).transpose().getOrThrow()
val (alice, genevieve) = nodes
assertThat(registrationHandler.idsPolled).containsOnly(
aliceName.organisation,
genevieveName.organisation,
notaryName.organisation)
// Check the nodes can communicate among themselves (and the notary).
val anonymous = false
genevieve.rpc.startFlow(
::CashIssueAndPaymentFlow,
1000.DOLLARS,
OpaqueBytes.of(12),
alice.nodeInfo.singleIdentity(),
anonymous,
defaultNotaryIdentity
).returnValue.getOrThrow()
}
}
@Test
fun `node registration wrong root cert`() {
val someCert = createSelfKeyAndSelfSignedCertificate().certificate.cert
val compatibilityZone = CompatibilityZoneParams(URL("http://$serverHostAndPort"), rootCert = someCert)
val someRootCert = X509Utilities.createSelfSignedCACertificate(
CordaX500Name("Integration Test Corda Node Root CA", "R3 Ltd", "London", "GB"),
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
val compatibilityZone = CompatibilityZoneParams(
URL("http://$serverHostAndPort"),
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
rootCert = someRootCert.cert)
internalDriver(
portAllocation = portAllocation,
notarySpecs = emptyList(),
compatibilityZone = compatibilityZone,
// Changing the content of the truststore makes the node fail in a number of ways if started out process.
startNodesInProcess = true
initialiseSerialization = false,
notarySpecs = listOf(NotarySpec(notaryName)),
startNodesInProcess = true // We need to run the nodes in the same process so that we can capture the correct exception
) {
assertThatThrownBy {
startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow()
}.isInstanceOf(WrongRootCertException::class.java)
defaultNotaryNode.getOrThrow()
}.isInstanceOf(CertPathValidatorException::class.java)
}
}
private fun createSelfKeyAndSelfSignedCertificate(): CertificateAndKeyPair {
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(
CordaX500Name(
commonName = "Integration Test Corda Node Root CA",
organisation = "R3 Ltd",
locality = "London",
country = "GB"),
rootCAKey)
return CertificateAndKeyPair(rootCACert, rootCAKey)
}
}
@Path("certificate")
@ -124,6 +155,7 @@ class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair)
certificationRequest,
rootCertAndKeyPair.keyPair,
arrayOf(rootCertAndKeyPair.certificate.cert))
require(!name.organisation.contains("\\s".toRegex())) { "Whitespace in the organisation name not supported" }
certPaths[name.organisation] = certPath
return Response.ok(name.organisation).build()
}
@ -154,13 +186,14 @@ class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair)
caCertPath: Array<Certificate>): Pair<CertPath, CordaX500Name> {
val request = JcaPKCS10CertificationRequest(certificationRequest)
val name = CordaX500Name.parse(request.subject.toString())
val x509CertificateHolder = X509Utilities.createCertificate(CertificateType.NODE_CA,
val nodeCaCert = X509Utilities.createCertificate(
CertificateType.NODE_CA,
caCertPath.first().toX509CertHolder(),
caKeyPair,
name,
request.publicKey,
nameConstraints = null)
val certPath = X509CertificateFactory().generateCertPath(x509CertificateHolder.cert, *caCertPath)
val certPath = X509CertificateFactory().generateCertPath(nodeCaCert.cert, *caCertPath)
return Pair(certPath, name)
}
}

View File

@ -25,7 +25,10 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.User
import net.corda.testing.*
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
import net.corda.testing.chooseIdentity
import net.corda.testing.internal.configureTestSSL
import net.corda.testing.node.internal.NodeBasedTest
import net.corda.testing.node.startFlow
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException

View File

@ -6,7 +6,7 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.testing.configureTestSSL
import net.corda.testing.internal.configureTestSSL
import org.apache.activemq.artemis.api.core.client.*
/**

View File

@ -60,7 +60,7 @@ import net.corda.node.services.vault.NodeVaultService
import net.corda.node.services.vault.VaultSoftLockManager
import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.AffinityExecutor
import net.corda.nodeapi.internal.IdentityGenerator
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.crypto.KeyStoreWrapper
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
@ -108,7 +108,7 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
// In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the
// AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in.
abstract class AbstractNode(val configuration: NodeConfiguration,
val platformClock: Clock,
val platformClock: CordaClock,
protected val versionInfo: VersionInfo,
protected val cordappLoader: CordappLoader,
private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
@ -184,13 +184,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
return SignedNodeInfo(serialised, listOf(signature))
}
open fun generateNodeInfo() {
open fun generateAndSaveNodeInfo(): NodeInfo {
check(started == null) { "Node has already been started" }
log.info("Generating nodeInfo ...")
initCertificate()
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database ->
return initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database ->
// TODO The fact that we need to specify an empty list of notaries just to generate our node info looks like
// a code smell.
val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList())
@ -200,6 +200,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
privateKey.sign(serialised.bytes)
}
NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo)
info
}
}
@ -239,14 +240,16 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
val notaryService = makeNotaryService(nodeServices, database)
val smm = makeStateMachineManager(database)
val flowStarter = FlowStarterImpl(serverThread, smm)
val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader)
val flowStarter = FlowStarterImpl(serverThread, smm, flowLogicRefFactory)
val schedulerService = NodeSchedulerService(
platformClock,
database,
flowStarter,
transactionStorage,
unfinishedSchedules = busyNodeLatch,
serverThread = serverThread)
serverThread = serverThread,
flowLogicRefFactory = flowLogicRefFactory)
if (serverThread is ExecutorService) {
runOnStop += {
// We wait here, even though any in-flight messages should have been drained away because the
@ -255,7 +258,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
MoreExecutors.shutdownAndAwaitTermination(serverThread as ExecutorService, 50, SECONDS)
}
}
makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService)
makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService, flowLogicRefFactory)
val rpcOps = makeRPCOps(flowStarter, database, smm)
startMessagingService(rpcOps)
installCoreFlows()
@ -263,7 +266,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
tokenizableServices = nodeServices + cordaServices + schedulerService
registerCordappFlows(smm)
_services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
FlowLogicRefFactoryImpl.classloader = cordappLoader.appClassLoader
startShell(rpcOps)
Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService)
}
@ -581,10 +583,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
protected open fun makeTransactionStorage(database: CordaPersistence): WritableTransactionStorage = DBTransactionStorage()
private fun makeVaultObservers(schedulerService: SchedulerService, hibernateConfig: HibernateConfiguration, smm: StateMachineManager, schemaService: SchemaService) {
private fun makeVaultObservers(schedulerService: SchedulerService, hibernateConfig: HibernateConfiguration, smm: StateMachineManager, schemaService: SchemaService, flowLogicRefFactory: FlowLogicRefFactory) {
VaultSoftLockManager.install(services.vaultService, smm)
ScheduledActivityObserver.install(services.vaultService, schedulerService)
ScheduledActivityObserver.install(services.vaultService, schedulerService, flowLogicRefFactory)
HibernateObserver.install(services.vaultService.rawUpdates, hibernateConfig, schemaService)
}
@ -724,7 +725,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword)
val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
val trustRoot = trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
val clientCa = caKeyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
val clientCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
val caCertificates = arrayOf(identityCert, clientCa.certificate.cert)
return PersistentIdentityService(trustRoot, *caCertificates)
}
@ -754,10 +755,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) {
// Node's main identity or if it's a single node notary
Pair(IdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName)
Pair(DevIdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName)
} else {
// The node is part of a distributed notary whose identity must already be generated beforehand.
Pair(IdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null)
Pair(DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null)
}
// TODO: Integrate with Key management service?
val privateKeyAlias = "$id-private-key"
@ -767,10 +768,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
"Unable to find in the key store the identity of the distributed notary ($id) the node is part of")
// TODO: Remove use of [IdentityGenerator.generateToDisk].
log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!")
keyStore.signAndSaveNewKeyPair(singleName, privateKeyAlias, generateKeyPair())
keyStore.storeLegalIdentity(singleName, privateKeyAlias, generateKeyPair())
}
val (x509Cert, keyPair) = keyStore.certificateAndKeyPair(privateKeyAlias)
val (x509Cert, keyPair) = keyStore.getCertificateAndKeyPair(privateKeyAlias)
// TODO: Use configuration to indicate composite key should be used instead of public key for the identity.
val compositeKeyAlias = "$id-composite-key"
@ -848,10 +849,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
}
internal class FlowStarterImpl(private val serverThread: AffinityExecutor, private val smm: StateMachineManager) : FlowStarter {
internal class FlowStarterImpl(private val serverThread: AffinityExecutor, private val smm: StateMachineManager, private val flowLogicRefFactory: FlowLogicRefFactory) : FlowStarter {
override fun <T> startFlow(logic: FlowLogic<T>, context: InvocationContext): CordaFuture<FlowStateMachine<T>> {
return serverThread.fetchFrom { smm.startFlow(logic, context) }
}
override fun <T> invokeFlowAsync(
logicType: Class<out FlowLogic<T>>,
context: InvocationContext,
vararg args: Any?): CordaFuture<FlowStateMachine<T>> {
val logicRef = flowLogicRefFactory.createForRPC(logicType, *args)
val logic: FlowLogic<T> = uncheckedCast(flowLogicRefFactory.toFlowLogic(logicRef))
return startFlow(logic, context)
}
}
class ConfigurationException(message: String) : CordaException(message)

View File

@ -22,10 +22,15 @@ abstract class CordaClock : Clock(), SerializeAsToken {
override fun getZone(): ZoneId = delegateClock.zone
@Deprecated("Do not use this. Instead seek to use ZonedDateTime methods.", level = DeprecationLevel.ERROR)
override fun withZone(zone: ZoneId) = throw UnsupportedOperationException("Tokenized clock does not support withZone()")
/** This is an observer on the mutation count of this [Clock], which reflects the occurrence of mutations. */
abstract val mutations: Observable<Long>
}
@ThreadSafe
class SimpleClock(override val delegateClock: Clock) : CordaClock()
class SimpleClock(override val delegateClock: Clock) : CordaClock() {
override val mutations: Observable<Long> = Observable.never()
}
/**
* An abstract class with helper methods for a type of Clock that might have it's concept of "now" adjusted externally.
@ -38,8 +43,7 @@ abstract class MutableClock(private var _delegateClock: Clock) : CordaClock() {
_delegateClock = clock
}
private val _version = AtomicLong(0L)
/** This is an observer on the mutation count of this [Clock], which reflects the occurence of mutations. */
val mutations: Observable<Long> by lazy {
override val mutations: Observable<Long> by lazy {
Observable.create { subscriber: Subscriber<in Long> ->
if (!subscriber.isUnsubscribed) {
mutationObservers.add(subscriber)

View File

@ -2,7 +2,6 @@ package net.corda.node.internal
import com.codahale.metrics.JmxReporter
import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.AuthServiceId
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.internal.uncheckedCast
@ -20,7 +19,9 @@ import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.services.api.SchemaService
import net.corda.node.services.config.*
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.SecurityConfiguration
import net.corda.node.services.config.VerifierType
import net.corda.node.services.messaging.*
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
import net.corda.node.utilities.AddressUtils
@ -33,6 +34,7 @@ import net.corda.nodeapi.internal.serialization.*
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import rx.Scheduler
import rx.schedulers.Schedulers
import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger
@ -67,7 +69,7 @@ open class Node(configuration: NodeConfiguration,
exitProcess(1)
}
private fun createClock(configuration: NodeConfiguration): Clock {
private fun createClock(configuration: NodeConfiguration): CordaClock {
return (if (configuration.useTestClock) ::DemoClock else ::SimpleClock)(Clock.systemUTC())
}
@ -269,15 +271,13 @@ open class Node(configuration: NodeConfiguration,
private val _startupComplete = openFuture<Unit>()
val startupComplete: CordaFuture<Unit> get() = _startupComplete
override fun generateNodeInfo() {
override fun generateAndSaveNodeInfo(): NodeInfo {
initialiseSerialization()
super.generateNodeInfo()
return super.generateAndSaveNodeInfo()
}
override fun start(): StartedNode<Node> {
if (initialiseSerialization) {
initialiseSerialization()
}
val started: StartedNode<Node> = uncheckedCast(super.start())
nodeReadyFuture.thenMatch({
serverThread.execute {
@ -310,8 +310,10 @@ open class Node(configuration: NodeConfiguration,
return started
}
override fun getRxIoScheduler() = Schedulers.io()!!
override fun getRxIoScheduler(): Scheduler = Schedulers.io()
private fun initialiseSerialization() {
if (!initialiseSerialization) return
val classloader = cordappLoader.appClassLoader
nodeSerializationEnv = SerializationEnvironmentImpl(
SerializationFactoryImpl().apply {

View File

@ -1,7 +1,6 @@
package net.corda.node.internal
import com.jcabi.manifests.Manifests
import com.typesafe.config.ConfigException
import joptsimple.OptionException
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.thenMatch
@ -126,7 +125,7 @@ open class NodeStartup(val args: Array<String>) {
val node = createNode(conf, versionInfo)
if (cmdlineOptions.justGenerateNodeInfo) {
// Perform the minimum required start-up logic to be able to write a nodeInfo to disk
node.generateNodeInfo()
node.generateAndSaveNodeInfo()
return
}
if (cmdlineOptions.justRunDbMigration) {

View File

@ -6,7 +6,6 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.uncheckedCast
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.node.NodeInfo
@ -21,7 +20,6 @@ import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.internal.cordapp.CordappProviderInternal
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -137,11 +135,7 @@ interface FlowStarter {
fun <T> invokeFlowAsync(
logicType: Class<out FlowLogic<T>>,
context: InvocationContext,
vararg args: Any?): CordaFuture<FlowStateMachine<T>> {
val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args)
val logic: FlowLogic<T> = uncheckedCast(FlowLogicRefFactoryImpl.toFlowLogic(logicRef))
return startFlow(logic, context)
}
vararg args: Any?): CordaFuture<FlowStateMachine<T>>
}
interface StartedNodeServices : ServiceHubInternal, FlowStarter

View File

@ -3,17 +3,21 @@ package net.corda.node.services.config
import com.typesafe.config.*
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import com.typesafe.config.ConfigRenderOptions
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.*
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.internal.toX509CertHolder
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.config.toProperties
import net.corda.nodeapi.internal.createDevKeyStores
import net.corda.nodeapi.internal.crypto.*
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.security.KeyStore
fun configOf(vararg pairs: Pair<String, Any?>): Config = ConfigFactory.parseMap(mapOf(*pairs))
operator fun Config.plus(overrides: Map<String, Any?>): Config = ConfigFactory.parseMap(overrides).withFallback(this)
@ -58,8 +62,10 @@ object ConfigHelper {
* Strictly for dev only automatically construct a server certificate/private key signed from
* the CA certs in Node resources. Then provision KeyStores into certificates folder under node path.
*/
// TODO Move this to KeyStoreConfigHelpers
fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName)
// TODO Move this to KeyStoreConfigHelpers
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
certificatesDirectory.createDirectories()
if (!trustStoreFile.exists()) {
@ -67,7 +73,9 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
}
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("certificates/cordadevcakeys.jks"), "cordacadevpass")
createKeystoreForCordaNode(sslKeystore, nodeKeystore, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName)
val rootCert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder()
val intermediateCa = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cordacadevkeypass")
createDevKeyStores(rootCert, intermediateCa, myLegalName)
// Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists.
val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"
@ -86,58 +94,3 @@ fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
}
}
}
/**
* An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine.
* @param sslKeyStorePath KeyStore path to save ssl key and cert to.
* @param clientCAKeystorePath KeyStore path to save client CA key and cert to.
* @param storePassword access password for KeyStore.
* @param keyPassword PrivateKey access password for the generated keys.
* It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same.
* @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore.
* @param caKeyPassword password to unlock private keys in the CA KeyStore.
* @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications.
*/
fun createKeystoreForCordaNode(sslKeyStorePath: Path,
clientCAKeystorePath: Path,
storePassword: String,
keyPassword: String,
caKeyStore: KeyStore,
caKeyPassword: String,
legalName: CordaX500Name,
signatureScheme: SignatureScheme = X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) {
val rootCACert = caKeyStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).toX509CertHolder()
val (intermediateCACert, intermediateCAKeyPair) = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caKeyPassword)
val clientKey = Crypto.generateKeyPair(signatureScheme)
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
val clientCACert = X509Utilities.createCertificate(CertificateType.NODE_CA,
intermediateCACert,
intermediateCAKeyPair,
legalName,
clientKey.public,
nameConstraints = nameConstraints)
val tlsKey = Crypto.generateKeyPair(signatureScheme)
val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, legalName, tlsKey.public)
val keyPass = keyPassword.toCharArray()
val clientCAKeystore = loadOrCreateKeyStore(clientCAKeystorePath, storePassword)
clientCAKeystore.addOrReplaceKey(
X509Utilities.CORDA_CLIENT_CA,
clientKey.private,
keyPass,
arrayOf(clientCACert, intermediateCACert, rootCACert))
clientCAKeystore.save(clientCAKeystorePath, storePassword)
val tlsKeystore = loadOrCreateKeyStore(sslKeyStorePath, storePassword)
tlsKeystore.addOrReplaceKey(
X509Utilities.CORDA_CLIENT_TLS,
tlsKey.private,
keyPass,
arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert))
tlsKeystore.save(sslKeyStorePath, storePassword)
}

View File

@ -10,6 +10,7 @@ import net.corda.core.contracts.ScheduledStateRef
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.internal.ThreadBox
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.concurrent.flatMap
@ -19,10 +20,10 @@ import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
import net.corda.node.internal.CordaClock
import net.corda.node.internal.MutableClock
import net.corda.node.services.api.FlowStarter
import net.corda.node.services.api.SchedulerService
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.PersistentMap
import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -55,13 +56,14 @@ import com.google.common.util.concurrent.SettableFuture as GuavaSettableFuture
* activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on.
*/
@ThreadSafe
class NodeSchedulerService(private val clock: Clock,
class NodeSchedulerService(private val clock: CordaClock,
private val database: CordaPersistence,
private val flowStarter: FlowStarter,
private val stateLoader: StateLoader,
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(),
private val unfinishedSchedules: ReusableLatch = ReusableLatch(),
private val serverThread: AffinityExecutor)
private val serverThread: AffinityExecutor,
private val flowLogicRefFactory: FlowLogicRefFactory)
: SchedulerService, SingletonSerializeAsToken() {
companion object {
@ -78,17 +80,13 @@ class NodeSchedulerService(private val clock: Clock,
@Suspendable
@VisibleForTesting
// We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name
fun awaitWithDeadline(clock: Clock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create<Any>()): Boolean {
fun awaitWithDeadline(clock: CordaClock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create<Any>()): Boolean {
var nanos: Long
do {
val originalFutureCompleted = makeStrandFriendlySettableFuture(future)
val subscription = if (clock is MutableClock) {
clock.mutations.first().subscribe {
val subscription = clock.mutations.first().subscribe {
originalFutureCompleted.set(false)
}
} else {
null
}
nanos = (clock.instant() until deadline).toNanos()
if (nanos > 0) {
try {
@ -102,7 +100,7 @@ class NodeSchedulerService(private val clock: Clock,
// No need to take action as will fall out of the loop due to future.isDone
}
}
subscription?.unsubscribe()
subscription.unsubscribe()
originalFutureCompleted.cancel(false)
} while (nanos > 0 && !future.isDone)
return future.isDone
@ -279,7 +277,7 @@ class NodeSchedulerService(private val clock: Clock,
scheduledStatesQueue.remove(scheduledState)
scheduledStatesQueue.add(newState)
} else {
val flowLogic = FlowLogicRefFactoryImpl.toFlowLogic(scheduledActivity.logicRef)
val flowLogic = flowLogicRefFactory.toFlowLogic(scheduledActivity.logicRef)
log.trace { "Scheduler starting FlowLogic $flowLogic" }
scheduledFlow = flowLogic
scheduledStates.remove(scheduledState.ref)
@ -297,7 +295,7 @@ class NodeSchedulerService(private val clock: Clock,
val state = txState.data as SchedulableState
return try {
// This can throw as running contract code.
state.nextScheduledActivity(scheduledState.ref, FlowLogicRefFactoryImpl)
state.nextScheduledActivity(scheduledState.ref, flowLogicRefFactory)
} catch (e: Exception) {
log.error("Attempt to run scheduled state $scheduledState resulted in error.", e)
null

View File

@ -4,19 +4,19 @@ import net.corda.core.contracts.ContractState
import net.corda.core.contracts.SchedulableState
import net.corda.core.contracts.ScheduledStateRef
import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.node.services.VaultService
import net.corda.node.services.api.SchedulerService
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
/**
* This observes the vault and schedules and unschedules activities appropriately based on state production and
* consumption.
*/
class ScheduledActivityObserver private constructor(private val schedulerService: SchedulerService) {
class ScheduledActivityObserver private constructor(private val schedulerService: SchedulerService, private val FlowLogicRefFactory: FlowLogicRefFactory) {
companion object {
@JvmStatic
fun install(vaultService: VaultService, schedulerService: SchedulerService) {
val observer = ScheduledActivityObserver(schedulerService)
fun install(vaultService: VaultService, schedulerService: SchedulerService, flowLogicRefFactory: FlowLogicRefFactory) {
val observer = ScheduledActivityObserver(schedulerService, flowLogicRefFactory)
vaultService.rawUpdates.subscribe { (consumed, produced) ->
consumed.forEach { schedulerService.unscheduleStateActivity(it.ref) }
produced.forEach { observer.scheduleStateActivity(it) }
@ -32,7 +32,7 @@ class ScheduledActivityObserver private constructor(private val schedulerService
private fun scheduleStateActivity(produced: StateAndRef<ContractState>) {
val producedState = produced.state.data
if (producedState is SchedulableState) {
val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactoryImpl)?.scheduledAt } ?: return
val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactory)?.scheduledAt } ?: return
schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt))
}
}

View File

@ -31,10 +31,8 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String,
* measure we might want the ability for the node admin to blacklist a flow such that it moves immediately to the "Flow Hospital"
* in response to a potential malicious use or buggy update to an app etc.
*/
object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory {
// TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now
var classloader: ClassLoader = javaClass.classLoader
// TODO: Replace with a per app classloader/cordapp provider/cordapp loader - this will do for now
class FlowLogicRefFactoryImpl(private val classloader: ClassLoader) : SingletonSerializeAsToken(), FlowLogicRefFactory {
override fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) {
throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow")
@ -42,7 +40,7 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor
return createForRPC(flowClass, *args)
}
fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
override fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
// TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly
// to avoid requiring only a single constructor.
val argTypes = args.map { it?.javaClass }
@ -81,7 +79,7 @@ object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactor
return FlowLogicRefImpl(type.name, args)
}
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
override fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface")
val klass = Class.forName(ref.flowLogicClassName, true, classloader).asSubclass(FlowLogic::class.java)
return createConstructor(klass, ref.args)()

View File

@ -12,10 +12,10 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter
import java.nio.file.Path
import java.security.KeyPair
import java.security.KeyStore
import java.security.cert.Certificate
import java.security.cert.X509Certificate
/**
* Helper for managing the node registration process, which checks for any existing certificates and requests them if
@ -32,7 +32,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
// TODO: Use different password for private key.
private val privateKeyPassword = config.keyStorePassword
private val trustStore: KeyStore
private val rootCert: Certificate
private val rootCert: X509Certificate
init {
require(config.trustStoreFile.exists()) {
@ -46,7 +46,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
"This file must contain the root CA cert of your compatibility zone. " +
"Please contact your CZ operator."
}
this.rootCert = rootCert
this.rootCert = rootCert as X509Certificate
}
/**
@ -94,11 +94,8 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
caKeyStore.save(config.nodeKeystore, keystorePassword)
println("Node private key and certificate stored in ${config.nodeKeystore}.")
// Check that the root of the signed certificate matches the expected certificate in the truststore.
if (rootCert != certificates.last()) {
// Assumes certificate chain always starts with client certificate and end with root certificate.
throw WrongRootCertException(rootCert, certificates.last(), config.trustStoreFile)
}
println("Checking root of the certificate path is what we expect.")
X509Utilities.validateCertificateChain(rootCert, *certificates)
println("Generating SSL certificate for node messaging service.")
val sslKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
@ -168,17 +165,3 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
}
}
}
/**
* Exception thrown when the doorman root certificate doesn't match the expected (out-of-band) root certificate.
* This usually means that there has been a Man-in-the-middle attack when contacting the doorman.
*/
class WrongRootCertException(expected: Certificate,
actual: Certificate,
expectedFilePath: Path):
Exception("""
The Root CA returned back from the registration process does not match the expected Root CA
expected: $expected
actual: $actual
the expected certificate is stored in: $expectedFilePath with alias $CORDA_ROOT_CA
""".trimMargin())

View File

@ -48,13 +48,15 @@ public class FlowLogicRefFromJavaTest {
}
}
private final FlowLogicRefFactoryImpl flowLogicRefFactory = new FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl.class.getClassLoader());
@Test
public void test() {
FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
flowLogicRefFactory.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
}
@Test
public void testNoArg() {
FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaNoArgFlowLogic.class);
flowLogicRefFactory.createForRPC(JavaNoArgFlowLogic.class);
}
}

View File

@ -34,47 +34,48 @@ class FlowLogicRefTest {
override fun call() = Unit
}
private val flowLogicRefFactory = FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl::class.java.classLoader)
@Test
fun `create kotlin no arg`() {
FlowLogicRefFactoryImpl.createForRPC(KotlinNoArgFlowLogic::class.java)
flowLogicRefFactory.createForRPC(KotlinNoArgFlowLogic::class.java)
}
@Test
fun `create kotlin`() {
val args = mapOf(Pair("A", ParamType1(1)), Pair("b", ParamType2("Hello Jack")))
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
}
@Test
fun `create primary`() {
FlowLogicRefFactoryImpl.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
flowLogicRefFactory.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
}
@Test
fun `create kotlin void`() {
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, emptyMap())
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, emptyMap())
}
@Test
fun `create kotlin non primary`() {
val args = mapOf(Pair("C", ParamType2("Hello Jack")))
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
}
@Test
fun `create java primitive no registration required`() {
val args = mapOf(Pair("primitive", "A string"))
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
}
@Test
fun `create kotlin primitive no registration required`() {
val args = mapOf(Pair("kotlinType", 3))
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
flowLogicRefFactory.createKotlin(KotlinFlowLogic::class.java, args)
}
@Test(expected = IllegalFlowLogicException::class)
fun `create for non-schedulable flow logic`() {
FlowLogicRefFactoryImpl.create(NonSchedulableFlow::class.java)
flowLogicRefFactory.create(NonSchedulableFlow::class.java)
}
}

View File

@ -64,6 +64,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val flowLogicRefFactory = FlowLogicRefFactoryImpl(FlowLogicRefFactoryImpl::class.java.classLoader)
private val realClock: Clock = Clock.systemUTC()
private val stoppedClock: Clock = Clock.fixed(realClock.instant(), realClock.zone)
private val testClock = TestClock(stoppedClock)
@ -122,7 +123,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
}
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)
mockSMM = StateMachineManagerImpl(services, DBCheckpointStorage(), smmExecutor, database, newSecureRandom())
scheduler = NodeSchedulerService(testClock, database, FlowStarterImpl(smmExecutor, mockSMM), validatedTransactions, schedulerGatedExecutor, serverThread = smmExecutor)
scheduler = NodeSchedulerService(testClock, database, FlowStarterImpl(smmExecutor, mockSMM, flowLogicRefFactory), validatedTransactions, schedulerGatedExecutor, serverThread = smmExecutor, flowLogicRefFactory = flowLogicRefFactory)
mockSMM.changes.subscribe { change ->
if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) {
smmHasRemovedAllFlows.countDown()
@ -305,7 +306,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
database.transaction {
apply {
val freshKey = kms.freshKey()
val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant, DUMMY_IDENTITY_1.party)
val state = TestState(flowLogicRefFactory.createForRPC(TestFlowLogic::class.java, increment), instant, DUMMY_IDENTITY_1.party)
val builder = TransactionBuilder(null).apply {
addOutputState(state, DummyContract.PROGRAM_ID, DUMMY_NOTARY)
addCommand(Command(), freshKey)

View File

@ -70,7 +70,7 @@ class NetworkMapClientTest {
// The test server returns same network parameter for any hash.
val networkParameter = networkMapClient.getNetworkParameter(SecureHash.randomSHA256())?.verified()
assertNotNull(networkParameter)
assertEquals(NetworkMapServer.stubNetworkParameter, networkParameter)
assertEquals(server.networkParameters, networkParameter)
}
@Test

View File

@ -8,6 +8,8 @@ import com.google.common.util.concurrent.SettableFuture
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.hours
import net.corda.core.utilities.minutes
import net.corda.node.internal.CordaClock
import net.corda.node.internal.SimpleClock
import net.corda.node.services.events.NodeSchedulerService
import net.corda.testing.node.TestClock
import org.junit.After
@ -25,13 +27,13 @@ import kotlin.test.fail
class ClockUtilsTest {
lateinit var realClock: Clock
lateinit var stoppedClock: Clock
lateinit var stoppedClock: CordaClock
lateinit var executor: ExecutorService
@Before
fun setup() {
realClock = Clock.systemUTC()
stoppedClock = Clock.fixed(realClock.instant(), realClock.zone)
stoppedClock = SimpleClock(Clock.fixed(realClock.instant(), realClock.zone))
executor = Executors.newSingleThreadExecutor()
}

View File

@ -14,12 +14,14 @@ import net.corda.core.internal.createDirectories
import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.crypto.*
import net.corda.testing.ALICE_NAME
import net.corda.testing.internal.createDevNodeCaCertPath
import net.corda.testing.internal.rigorousMock
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.security.cert.CertPathValidatorException
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import kotlin.test.assertFalse
@ -29,16 +31,19 @@ class NetworkRegistrationHelperTest {
private val fs = Jimfs.newFileSystem(unix())
private val requestId = SecureHash.randomSHA256().toString()
private val nodeLegalName = ALICE_NAME
private val intermediateCaName = CordaX500Name("CORDA_INTERMEDIATE_CA", "R3 Ltd", "London", "GB")
private val rootCaName = CordaX500Name("CORDA_ROOT_CA", "R3 Ltd", "London", "GB")
private val nodeCaCert = createCaCert(nodeLegalName)
private val intermediateCaCert = createCaCert(intermediateCaName)
private val rootCaCert = createCaCert(rootCaName)
private lateinit var rootCaCert: X509Certificate
private lateinit var intermediateCaCert: X509Certificate
private lateinit var nodeCaCert: X509Certificate
private lateinit var config: NodeConfiguration
@Before
fun init() {
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(nodeLegalName)
this.rootCaCert = rootCa.certificate.cert
this.intermediateCaCert = intermediateCa.certificate.cert
this.nodeCaCert = nodeCa.certificate.cert
val baseDirectory = fs.getPath("/baseDir").createDirectories()
abstract class AbstractNodeConfiguration : NodeConfiguration
config = rigorousMock<AbstractNodeConfiguration>().also {
@ -108,11 +113,13 @@ class NetworkRegistrationHelperTest {
@Test
fun `wrong root cert in truststore`() {
saveTrustStoreWithRootCa(createCaCert(CordaX500Name("Foo", "MU", "GB")))
val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name("Foo", "MU", "GB"), rootKeyPair)
saveTrustStoreWithRootCa(rootCert.cert)
val registrationHelper = createRegistrationHelper()
assertThatThrownBy {
registrationHelper.buildKeystore()
}.isInstanceOf(WrongRootCertException::class.java)
}.isInstanceOf(CertPathValidatorException::class.java)
}
private fun createRegistrationHelper(): NetworkRegistrationHelper {
@ -123,15 +130,11 @@ class NetworkRegistrationHelperTest {
return NetworkRegistrationHelper(config, certService)
}
private fun saveTrustStoreWithRootCa(rootCa: X509Certificate) {
config.trustStoreFile.parent.createDirectories()
private fun saveTrustStoreWithRootCa(rootCert: X509Certificate) {
config.certificatesDirectory.createDirectories()
loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also {
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCa)
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
it.save(config.trustStoreFile, config.trustStorePassword)
}
}
private fun createCaCert(name: CordaX500Name): X509Certificate {
return X509Utilities.createSelfSignedCACertificate(name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert
}
}

View File

@ -21,7 +21,7 @@ define([
$scope.formError = resp.data;
}, handleHttpFail);
};
$('input.percent').mask("9.999999%", {placeholder: "", autoclear: false});
$('input.percent').mask("9.999999", {placeholder: "", autoclear: false});
$('#swapirscolumns').click(() => {
let first = $('#irscolumns .irscolumn:eq( 0 )');
let last = $('#irscolumns .irscolumn:eq( 1 )');

View File

@ -8,7 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.config.BFTSMaRtConfiguration
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.transactions.minCorrectReplicas
import net.corda.nodeapi.internal.IdentityGenerator
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.testing.node.internal.demorun.*
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
@ -62,7 +62,7 @@ class BFTNotaryCordform : CordformDefinition() {
}
override fun setup(context: CordformContext) {
IdentityGenerator.generateDistributedNotaryIdentity(
DevIdentityGenerator.generateDistributedNotaryIdentity(
notaryNames.map { context.baseDirectory(it.toString()) },
clusterName,
minCorrectReplicas(clusterSize)

View File

@ -4,11 +4,10 @@ import net.corda.cordform.CordformContext
import net.corda.cordform.CordformDefinition
import net.corda.cordform.CordformNode
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.services.NotaryService
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.config.NotaryConfig
import net.corda.node.services.config.RaftConfig
import net.corda.nodeapi.internal.IdentityGenerator
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.testing.node.internal.demorun.*
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
@ -59,7 +58,7 @@ class RaftNotaryCordform : CordformDefinition() {
}
override fun setup(context: CordformContext) {
IdentityGenerator.generateDistributedNotaryIdentity(
DevIdentityGenerator.generateDistributedNotaryIdentity(
notaryNames.map { context.baseDirectory(it.toString()) },
clusterName
)

View File

@ -72,6 +72,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
notary = [validating : true]
p2pPort 10002
cordapps = ["$project.group:finance:$corda_release_version"]
extraConfig = [
jvmArgs : [ "-Xmx1g"]
]
}
node {
name "O=Bank A,L=London,C=GB"
@ -80,6 +83,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
rpcPort 10006
cordapps = ["$project.group:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
extraConfig = [
jvmArgs : [ "-Xmx1g"]
]
}
node {
name "O=Bank B,L=New York,C=US"
@ -88,6 +94,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
rpcPort 10009
cordapps = ["$project.group:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
extraConfig = [
jvmArgs : [ "-Xmx1g"]
]
}
node {
name "O=Bank C,L=Tokyo,C=JP"
@ -96,6 +105,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
rpcPort 10012
cordapps = ["$project.group:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
extraConfig = [
jvmArgs : [ "-Xmx1g"]
]
}
}

View File

@ -40,7 +40,8 @@ class SellerFlow(private val otherParty: Party,
progressTracker.currentStep = SELF_ISSUING
val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java).states.first()
val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java)
.states.firstOrNull() ?: throw IllegalStateException("No commercial paper found. Please check if you issued the papers first, follow the README for instructions.")
progressTracker.currentStep = TRADING

View File

@ -2,6 +2,7 @@ package net.corda.testing.driver
import net.corda.core.concurrent.CordaFuture
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.core.internal.div
import net.corda.core.internal.list
import net.corda.core.internal.readLines
@ -47,12 +48,28 @@ class DriverTests : IntegrationTest() {
@Test
fun `simple node startup and shutdown`() {
val handle = driver {
val regulator = startNode(providedName = DUMMY_REGULATOR_NAME)
nodeMustBeUp(regulator)
val node = startNode(providedName = DUMMY_REGULATOR_NAME)
nodeMustBeUp(node)
}
nodeMustBeDown(handle)
}
@Test
fun `starting with default notary`() {
driver {
// Make sure the default is a single-node notary
val notary = defaultNotaryNode.getOrThrow()
val notaryIdentities = notary.nodeInfo.legalIdentitiesAndCerts
// Make sure the notary node has only one identity
assertThat(notaryIdentities).hasSize(1)
val identity = notaryIdentities[0]
// Make sure this identity is a legal identity, like it is for normal nodes.
assertThat(CertRole.extract(identity.certificate)).isEqualTo(CertRole.LEGAL_IDENTITY)
// And make sure this identity is published as the notary identity (via the network parameters)
assertThat(notary.rpc.notaryIdentities()).containsOnly(identity.party)
}
}
@Test
fun `random free port allocation`() {
val nodeHandle = driver(portAllocation = PortAllocation.RandomFree) {

View File

@ -14,11 +14,11 @@ import net.corda.node.internal.StartedNode
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.VerifierType
import net.corda.nodeapi.internal.config.User
import net.corda.testing.DUMMY_NOTARY_NAME
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.internal.DriverDSLImpl
import net.corda.testing.node.internal.genericDriver
import net.corda.testing.node.internal.getTimestampAsDirectoryName
import net.corda.testing.DUMMY_NOTARY_NAME
import net.corda.testing.node.NotarySpec
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.nio.file.Path

View File

@ -38,7 +38,7 @@ import net.corda.node.services.transactions.BFTSMaRt
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import net.corda.nodeapi.internal.IdentityGenerator
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NotaryInfo
@ -236,7 +236,7 @@ open class MockNetwork(private val cordappPackages: List<String>,
private fun generateNotaryIdentities(): List<NotaryInfo> {
return notarySpecs.mapIndexed { index, (name, validating) ->
val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(nextNodeId + index), name)
val identity = DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(nextNodeId + index), name)
NotaryInfo(identity, validating)
}
}

View File

@ -10,15 +10,12 @@ import net.corda.cordform.CordformContext
import net.corda.cordform.CordformNode
import net.corda.core.concurrent.CordaFuture
import net.corda.core.concurrent.firstOf
import net.corda.core.crypto.random63BitValue
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.ThreadBox
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.*
import net.corda.core.internal.copyTo
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.serialization.deserialize
import net.corda.core.toFuture
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
@ -31,7 +28,8 @@ import net.corda.node.services.Permissions
import net.corda.node.services.config.*
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.nodeapi.internal.IdentityGenerator
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.parseAs
@ -68,7 +66,7 @@ import java.nio.file.StandardCopyOption
import java.security.cert.X509Certificate
import java.time.Duration
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZoneOffset.UTC
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.Executors
@ -96,15 +94,16 @@ class DriverDSLImpl(
private var _shutdownManager: ShutdownManager? = null
override val shutdownManager get() = _shutdownManager!!
private val cordappPackages = extraCordappPackagesToScan + getCallerPackage()
// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/
// This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system.
// Investigate whether we can avoid that.
private var nodeInfoFilesCopier: NodeInfoFilesCopier? = null
// Map from a nodes legal name to an observable emitting the number of nodes in its network map.
private val countObservables = mutableMapOf<CordaX500Name, Observable<Int>>()
private lateinit var _notaries: List<NotaryHandle>
override val notaryHandles: List<NotaryHandle> get() = _notaries
private var networkParameters: NetworkParametersCopier? = null
/**
* Future which completes when the network map is available, whether a local one or one from the CZ. This future acts
* as a gate to prevent nodes from starting too early. The value of the future is a [LocalNetworkMap] object, which
* is null if the network map is being provided by the CZ.
*/
private lateinit var networkMapAvailability: CordaFuture<LocalNetworkMap?>
private lateinit var _notaries: CordaFuture<List<NotaryHandle>>
override val notaryHandles: List<NotaryHandle> get() = _notaries.getOrThrow()
class State {
val processes = ArrayList<Process>()
@ -145,12 +144,12 @@ class DriverDSLImpl(
_executorService?.shutdownNow()
}
private fun establishRpc(config: NodeConfiguration, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
val rpcAddress = config.rpcAddress!!
private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture<out Process>): CordaFuture<CordaRPCOps> {
val rpcAddress = config.corda.rpcAddress!!
val client = CordaRPCClient(rpcAddress)
val connectionFuture = poll(executorService, "RPC connection") {
try {
client.start(config.rpcUsers[0].username, config.rpcUsers[0].password)
config.corda.rpcUsers[0].run { client.start(username, password) }
} catch (e: Exception) {
if (processDeathFuture.isDone) throw e
log.error("Exception $e, Retrying RPC connection at $rpcAddress")
@ -178,19 +177,38 @@ class DriverDSLImpl(
): CordaFuture<NodeHandle> {
val p2pAddress = portAllocation.nextHostAndPort()
// TODO: Derive name from the full picked name, don't just wrap the common name
val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB")
val name = providedName ?: CordaX500Name("${oneOf(names).organisation}-${p2pAddress.port}", "London", "GB")
val registrationFuture = if (compatibilityZone?.rootCert != null) {
nodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url)
// We don't need the network map to be available to be able to register the node
startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.url)
} else {
doneFuture(Unit)
}
return registrationFuture.flatMap {
networkMapAvailability.flatMap {
// But starting the node proper does require the network map
startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress)
}
}
}
private fun startRegisteredNode(name: CordaX500Name,
localNetworkMap: LocalNetworkMap?,
rpcUsers: List<User>,
verifierType: VerifierType,
customOverrides: Map<String, Any?>,
startInSameProcess: Boolean? = null,
maximumHeapSize: String = "200m",
p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort()): CordaFuture<NodeHandle> {
val rpcAddress = portAllocation.nextHostAndPort()
val webAddress = portAllocation.nextHostAndPort()
val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) }
val configMap = configOf(
val czUrlConfig = if (compatibilityZone != null) mapOf("compatibilityZoneURL" to compatibilityZone.url.toString()) else emptyMap()
val config = NodeConfig(ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = configOf(
"myLegalName" to name.toString(),
"p2pAddress" to p2pAddress.toString(),
"rpcAddress" to rpcAddress.toString(),
@ -198,45 +216,35 @@ class DriverDSLImpl(
"useTestClock" to useTestClock,
"rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() },
"verifierType" to verifierType.name
) + customOverrides
val config = ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = if (compatibilityZone != null) {
configMap + mapOf("compatibilityZoneURL" to compatibilityZone.url.toString())
} else {
configMap
}
)
startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize)
}
) + czUrlConfig + customOverrides
))
return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize, localNetworkMap)
}
private fun nodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<Unit> {
private fun startNodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<NodeConfig> {
val baseDirectory = baseDirectory(providedName).createDirectories()
val config = ConfigHelper.loadConfig(
val config = NodeConfig(ConfigHelper.loadConfig(
baseDirectory = baseDirectory,
allowMissingConfig = true,
configOverrides = configOf(
"p2pAddress" to "localhost:1222", // required argument, not really used
"compatibilityZoneURL" to compatibilityZoneURL.toString(),
"myLegalName" to providedName.toString())
)
val configuration = config.parseAsNodeConfiguration()
))
configuration.trustStoreFile.parent.createDirectories()
loadOrCreateKeyStore(configuration.trustStoreFile, configuration.trustStorePassword).also {
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
it.save(configuration.trustStoreFile, configuration.trustStorePassword)
config.corda.certificatesDirectory.createDirectories()
loadOrCreateKeyStore(config.corda.trustStoreFile, config.corda.trustStorePassword).apply {
addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
save(config.corda.trustStoreFile, config.corda.trustStorePassword)
}
return if (startNodesInProcess) {
// This is a bit cheating, we're not starting a full node, we're just calling the code nodes call
// when registering.
NetworkRegistrationHelper(configuration, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore()
doneFuture(Unit)
executorService.fork {
NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore()
config
}
} else {
startOutOfProcessNodeRegistration(config, configuration)
startOutOfProcessMiniNode(config, "--initial-registration").map { config }
}
}
@ -247,7 +255,9 @@ class DriverDSLImpl(
}
internal fun startCordformNodes(cordforms: List<CordformNode>): CordaFuture<*> {
check(compatibilityZone == null) { "Cordform nodes should be run without compatibilityZone configuration" }
check(notarySpecs.isEmpty()) { "Specify notaries in the CordformDefinition" }
check(compatibilityZone == null) { "Cordform nodes cannot be run with compatibilityZoneURL" }
val clusterNodes = HashMultimap.create<ClusterType, CordaX500Name>()
val notaryInfos = ArrayList<NotaryInfo>()
@ -266,23 +276,23 @@ class DriverDSLImpl(
clusterNodes.put(ClusterType.NON_VALIDATING_BFT, name)
} else {
// We have all we need here to generate the identity for single node notaries
val identity = IdentityGenerator.generateNodeIdentity(baseDirectory(name), legalName = name)
val identity = DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(name), legalName = name)
notaryInfos += NotaryInfo(identity, notaryConfig.validating)
}
}
clusterNodes.asMap().forEach { type, nodeNames ->
val identity = IdentityGenerator.generateDistributedNotaryIdentity(
val identity = DevIdentityGenerator.generateDistributedNotaryIdentity(
dirs = nodeNames.map { baseDirectory(it) },
notaryName = type.clusterName
)
notaryInfos += NotaryInfo(identity, type.validating)
}
networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos))
val localNetworkMap = LocalNetworkMap(notaryInfos)
return cordforms.map {
val startedNode = startCordformNode(it)
val startedNode = startCordformNode(it, localNetworkMap)
if (it.webAddress != null) {
// Start a webserver if an address for it was specified
startedNode.flatMap { startWebserver(it) }
@ -292,21 +302,21 @@ class DriverDSLImpl(
}.transpose()
}
private fun startCordformNode(cordform: CordformNode): CordaFuture<NodeHandle> {
private fun startCordformNode(cordform: CordformNode, localNetworkMap: LocalNetworkMap): CordaFuture<NodeHandle> {
val name = CordaX500Name.parse(cordform.name)
// TODO We shouldn't have to allocate an RPC or web address if they're not specified. We're having to do this because of startNodeInternal
val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap()
val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort()
val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap()
val rpcUsers = cordform.rpcUsers
val config = ConfigHelper.loadConfig(
val config = NodeConfig(ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = cordform.config + rpcAddress + notary + mapOf(
"rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers
)
)
return startNodeInternal(config, webAddress, null, "200m")
))
return startNodeInternal(config, webAddress, null, "200m", localNetworkMap)
}
private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle {
@ -340,43 +350,92 @@ class DriverDSLImpl(
}
_executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build())
_shutdownManager = ShutdownManager(executorService)
if (compatibilityZone == null) {
// Without a compatibility zone URL we have to copy the node info files ourselves to make sure the nodes see each other
nodeInfoFilesCopier = NodeInfoFilesCopier().also {
shutdownManager.registerShutdown(it::close)
val notaryInfosFuture = if (compatibilityZone == null) {
// If no CZ is specified then the driver does the generation of the network parameters and the copying of the
// node info files.
startNotaryIdentityGeneration().map { notaryInfos -> Pair(notaryInfos, LocalNetworkMap(notaryInfos)) }
} else {
// Otherwise it's the CZ's job to distribute thse via the HTTP network map, as that is what the nodes will be expecting.
val notaryInfosFuture = if (compatibilityZone.rootCert == null) {
// No root cert specified so we use the dev root cert to generate the notary identities.
startNotaryIdentityGeneration()
} else {
// With a root cert specified we delegate generation of the notary identities to the CZ.
startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone.url)
}
notaryInfosFuture.map { notaryInfos ->
compatibilityZone.publishNotaries(notaryInfos)
Pair(notaryInfos, null)
}
val notaryInfos = generateNotaryIdentities()
// The network parameters must be serialised before starting any of the nodes
if (compatibilityZone == null) networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos))
val nodeHandles = startNotaries()
_notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) }
}
private fun generateNotaryIdentities(): List<NotaryInfo> {
return notarySpecs.map { spec ->
networkMapAvailability = notaryInfosFuture.map { it.second }
_notaries = notaryInfosFuture.map { (notaryInfos, localNetworkMap) ->
val listOfFutureNodeHandles = startNotaries(localNetworkMap)
notaryInfos.zip(listOfFutureNodeHandles) { (identity, validating), nodeHandlesFuture ->
NotaryHandle(identity, validating, nodeHandlesFuture)
}
}
}
private fun startNotaryIdentityGeneration(): CordaFuture<List<NotaryInfo>> {
return executorService.fork {
notarySpecs.map { spec ->
val identity = if (spec.cluster == null) {
IdentityGenerator.generateNodeIdentity(baseDirectory(spec.name), spec.name, compatibilityZone?.rootCert)
DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name)
} else {
IdentityGenerator.generateDistributedNotaryIdentity(
DevIdentityGenerator.generateDistributedNotaryIdentity(
dirs = generateNodeNames(spec).map { baseDirectory(it) },
notaryName = spec.name,
customRootCert = compatibilityZone?.rootCert
notaryName = spec.name
)
}
NotaryInfo(identity, spec.validating)
}
}
}
private fun startAllNotaryRegistrations(rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<List<NotaryInfo>> {
// Start the registration process for all the notaries together then wait for their responses.
return notarySpecs.map { spec ->
require(spec.cluster == null) { "Registering distributed notaries not supported" }
startNotaryRegistration(spec, rootCert, compatibilityZoneURL)
}.transpose()
}
private fun startNotaryRegistration(spec: NotarySpec, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<NotaryInfo> {
return startNodeRegistration(spec.name, rootCert, compatibilityZoneURL).flatMap { config ->
// Node registration only gives us the node CA cert, not the identity cert. That is only created on first
// startup or when the node is told to just generate its node info file. We do that here.
if (startNodesInProcess) {
executorService.fork {
val nodeInfo = Node(config.corda, MOCK_VERSION_INFO, initialiseSerialization = false).generateAndSaveNodeInfo()
NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)
}
} else {
// TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper
// This causes two node info files to be generated.
startOutOfProcessMiniNode(config, "--just-generate-node-info").map {
// Once done we have to read the signed node info file that's been generated
val nodeInfoFile = config.corda.baseDirectory.list { paths ->
paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
}
val nodeInfo = nodeInfoFile.readAll().deserialize<SignedNodeInfo>().verified()
NotaryInfo(nodeInfo.legalIdentities[0], spec.validating)
}
}
}
}
private fun generateNodeNames(spec: NotarySpec): List<CordaX500Name> {
return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(organisation = "${spec.name.organisation}-$it") }
}
private fun startNotaries(): List<CordaFuture<List<NodeHandle>>> {
private fun startNotaries(localNetworkMap: LocalNetworkMap?): List<CordaFuture<List<NodeHandle>>> {
return notarySpecs.map {
when {
it.cluster == null -> startSingleNotary(it)
it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it)
it.cluster == null -> startSingleNotary(it, localNetworkMap)
it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it, localNetworkMap)
else -> throw IllegalArgumentException("BFT-SMaRt not supported")
}
}
@ -386,16 +445,17 @@ class DriverDSLImpl(
// generating the configs for the nodes, probably making use of Any.toConfig()
private fun NotaryConfig.toConfigMap(): Map<String, Any> = mapOf("notary" to toConfig().root().unwrapped())
private fun startSingleNotary(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
return startNode(
providedName = spec.name,
rpcUsers = spec.rpcUsers,
verifierType = spec.verifierType,
private fun startSingleNotary(spec: NotarySpec, localNetworkMap: LocalNetworkMap?): CordaFuture<List<NodeHandle>> {
return startRegisteredNode(
spec.name,
localNetworkMap,
spec.rpcUsers,
spec.verifierType,
customOverrides = NotaryConfig(spec.validating).toConfigMap()
).map { listOf(it) }
}
private fun startRaftNotaryCluster(spec: NotarySpec): CordaFuture<List<NodeHandle>> {
private fun startRaftNotaryCluster(spec: NotarySpec, localNetworkMap: LocalNetworkMap?): CordaFuture<List<NodeHandle>> {
fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map<String, Any> {
val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList()
val config = NotaryConfig(
@ -408,20 +468,22 @@ class DriverDSLImpl(
val clusterAddress = portAllocation.nextHostAndPort()
// Start the first node that will bootstrap the cluster
val firstNodeFuture = startNode(
providedName = nodeNames[0],
rpcUsers = spec.rpcUsers,
verifierType = spec.verifierType,
val firstNodeFuture = startRegisteredNode(
nodeNames[0],
localNetworkMap,
spec.rpcUsers,
spec.verifierType,
customOverrides = notaryConfig(clusterAddress)
)
// All other nodes will join the cluster
val restNodeFutures = nodeNames.drop(1).map {
val nodeAddress = portAllocation.nextHostAndPort()
startNode(
providedName = it,
rpcUsers = spec.rpcUsers,
verifierType = spec.verifierType,
startRegisteredNode(
it,
localNetworkMap,
spec.rpcUsers,
spec.verifierType,
customOverrides = notaryConfig(nodeAddress, clusterAddress)
)
}
@ -436,8 +498,6 @@ class DriverDSLImpl(
return driverDirectory / nodeDirectoryName
}
override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName))
/**
* @param initial number of nodes currently in the network map of a running node.
* @param networkMapCacheChangeObservable an observable returning the updates to the node network map.
@ -447,7 +507,7 @@ class DriverDSLImpl(
private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable<NetworkMapCache.MapChange>):
ConnectableObservable<Int> {
val count = AtomicInteger(initial)
return networkMapCacheChangeObservable.map { it ->
return networkMapCacheChangeObservable.map {
when (it) {
is NetworkMapCache.MapChange.Added -> count.incrementAndGet()
is NetworkMapCache.MapChange.Removed -> count.decrementAndGet()
@ -482,31 +542,44 @@ class DriverDSLImpl(
return future
}
private fun startOutOfProcessNodeRegistration(config: Config, configuration: NodeConfiguration): CordaFuture<Unit> {
/**
* Start the node with the given flag which is expected to start the node for some function, which once complete will
* terminate the node.
*/
private fun startOutOfProcessMiniNode(config: NodeConfig, extraCmdLineFlag: String): CordaFuture<Unit> {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null
val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort,
systemProperties, cordappPackages, "200m", initialRegistration = true)
val process = startOutOfProcessNode(
config,
quasarJarPath,
debugPort,
jolokiaJarPath,
monitorPort,
systemProperties,
cordappPackages,
"200m",
extraCmdLineFlag
)
return poll(executorService, "node registration (${configuration.myLegalName})") {
return poll(executorService, "$extraCmdLineFlag (${config.corda.myLegalName})") {
if (process.isAlive) null else Unit
}
}
private fun startNodeInternal(config: Config,
private fun startNodeInternal(config: NodeConfig,
webAddress: NetworkHostAndPort,
startInProcess: Boolean?,
maximumHeapSize: String): CordaFuture<NodeHandle> {
val configuration = config.parseAsNodeConfiguration()
val baseDirectory = configuration.baseDirectory.createDirectories()
nodeInfoFilesCopier?.addConfig(baseDirectory)
networkParameters?.install(baseDirectory)
maximumHeapSize: String,
localNetworkMap: LocalNetworkMap?): CordaFuture<NodeHandle> {
val baseDirectory = config.corda.baseDirectory.createDirectories()
localNetworkMap?.networkParametersCopier?.install(baseDirectory)
localNetworkMap?.nodeInfosCopier?.addConfig(baseDirectory)
val onNodeExit: () -> Unit = {
nodeInfoFilesCopier?.removeConfig(baseDirectory)
countObservables.remove(configuration.myLegalName)
localNetworkMap?.nodeInfosCopier?.removeConfig(baseDirectory)
countObservables.remove(config.corda.myLegalName)
}
if (startInProcess ?: startNodesInProcess) {
val nodeAndThreadFuture = startInProcessNode(executorService, configuration, config, cordappPackages)
val nodeAndThreadFuture = startInProcessNode(executorService, config, cordappPackages)
shutdownManager.registerShutdown(
nodeAndThreadFuture.map { (node, thread) ->
{
@ -516,16 +589,16 @@ class DriverDSLImpl(
}
)
return nodeAndThreadFuture.flatMap { (node, thread) ->
establishRpc(configuration, openFuture()).flatMap { rpc ->
establishRpc(config, openFuture()).flatMap { rpc ->
allNodesConnected(rpc).map {
NodeHandle.InProcess(rpc.nodeInfo(), rpc, configuration, webAddress, node, thread, onNodeExit)
NodeHandle.InProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, node, thread, onNodeExit)
}
}
}
} else {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null
val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, initialRegistration = false)
val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, null)
if (waitForNodesToFinish) {
state.locked {
processes += process
@ -533,22 +606,21 @@ class DriverDSLImpl(
} else {
shutdownManager.registerProcessShutdown(process)
}
val p2pReadyFuture = addressMustBeBoundFuture(executorService, configuration.p2pAddress, process)
val p2pReadyFuture = addressMustBeBoundFuture(executorService, config.corda.p2pAddress, process)
return p2pReadyFuture.flatMap {
val processDeathFuture = poll(executorService, "process death while waiting for RPC (${configuration.myLegalName})") {
val processDeathFuture = poll(executorService, "process death while waiting for RPC (${config.corda.myLegalName})") {
if (process.isAlive) null else process
}
establishRpc(configuration, processDeathFuture).flatMap { rpc ->
establishRpc(config, processDeathFuture).flatMap { rpc ->
// Check for all nodes to have all other nodes in background in case RPC is failing over:
val networkMapFuture = executorService.fork { allNodesConnected(rpc) }.flatMap { it }
firstOf(processDeathFuture, networkMapFuture) {
if (it == processDeathFuture) {
throw ListenProcessDeathException(configuration.p2pAddress, process)
throw ListenProcessDeathException(config.corda.p2pAddress, process)
}
processDeathFuture.cancel(false)
log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress")
NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, configuration, webAddress, debugPort, process,
onNodeExit)
NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, debugPort, process, onNodeExit)
}
}
}
@ -561,6 +633,25 @@ class DriverDSLImpl(
return pollFuture
}
/**
* The local version of the network map, which is a bunch of classes that copy the relevant files to the node directories.
*/
private inner class LocalNetworkMap(notaryInfos: List<NotaryInfo>) {
val networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaryInfos))
// TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/
// This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system.
// Investigate whether we can avoid that.
val nodeInfosCopier = NodeInfoFilesCopier().also { shutdownManager.registerShutdown(it::close) }
}
/**
* Simple holder class to capture the node configuration both as the raw [Config] object and the parsed [NodeConfiguration].
* Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration].
*/
private class NodeConfig(val typesafe: Config) {
val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration()
}
companion object {
internal val log = contextLogger()
@ -588,28 +679,26 @@ class DriverDSLImpl(
private fun startInProcessNode(
executorService: ScheduledExecutorService,
nodeConf: NodeConfiguration,
config: Config,
config: NodeConfig,
cordappPackages: List<String>
): CordaFuture<Pair<StartedNode<Node>, Thread>> {
return executorService.fork {
log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}")
log.info("Starting in-process Node ${config.corda.myLegalName.organisation}")
// Write node.conf
writeConfig(nodeConf.baseDirectory, "node.conf", config)
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe)
// TODO pass the version in?
val node = InProcessNode(nodeConf, MOCK_VERSION_INFO, cordappPackages).start()
val nodeThread = thread(name = nodeConf.myLegalName.organisation) {
val node = InProcessNode(config.corda, MOCK_VERSION_INFO, cordappPackages).start()
val nodeThread = thread(name = config.corda.myLegalName.organisation) {
node.internals.run()
}
node to nodeThread
}.flatMap { nodeAndThread ->
addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread }
addressMustBeBoundFuture(executorService, config.corda.p2pAddress).map { nodeAndThread }
}
}
private fun startOutOfProcessNode(
nodeConf: NodeConfiguration,
config: Config,
config: NodeConfig,
quasarJarPath: String,
debugPort: Int?,
jolokiaJarPath: String,
@ -617,15 +706,17 @@ class DriverDSLImpl(
overriddenSystemProperties: Map<String, String>,
cordappPackages: List<String>,
maximumHeapSize: String,
initialRegistration: Boolean
extraCmdLineFlag: String?
): Process {
log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled") + ", jolokia monitoring port is " + (monitorPort ?: "not enabled"))
log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, " +
"debug port is " + (debugPort ?: "not enabled") + ", " +
"jolokia monitoring port is " + (monitorPort ?: "not enabled"))
// Write node.conf
writeConfig(nodeConf.baseDirectory, "node.conf", config)
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe)
val systemProperties = mutableMapOf(
"name" to nodeConf.myLegalName,
"visualvm.display.name" to "corda-${nodeConf.myLegalName}",
"name" to config.corda.myLegalName,
"visualvm.display.name" to "corda-${config.corda.myLegalName}",
"java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process
"log4j2.debug" to if(debugPort != null) "true" else "false"
)
@ -643,18 +734,18 @@ class DriverDSLImpl(
"io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" +
"org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" +
"org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" +
"org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)"
"org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;)"
val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } +
"-javaagent:$quasarJarPath=$excludePattern"
val jolokiaAgent = monitorPort?.let { "-javaagent:$jolokiaJarPath=port=$monitorPort,host=localhost" }
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
val arguments = mutableListOf(
"--base-directory=${nodeConf.baseDirectory}",
"--base-directory=${config.corda.baseDirectory}",
"--logging-level=$loggingLevel",
"--no-local-shell").also {
if (initialRegistration) {
it += "--initial-registration"
if (extraCmdLineFlag != null) {
it += extraCmdLineFlag
}
}.toList()
@ -663,8 +754,8 @@ class DriverDSLImpl(
arguments = arguments,
jdwpPort = debugPort,
extraJvmArguments = extraJvmArguments + listOfNotNull(jolokiaAgent),
errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log",
workingDirectory = nodeConf.baseDirectory,
errorLogPath = config.corda.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log",
workingDirectory = config.corda.baseDirectory,
maximumHeapSize = maximumHeapSize
)
}
@ -831,11 +922,16 @@ fun <DI : DriverDSL, D : InternalDriverDSL, A> genericDriver(
}
/**
* Internal API to enable testing of the network map service and node registration process using the internal driver.
* @property url The base CZ URL for registration and network map updates
* @property rootCert If specified then the node will register itself using [url] and expect the registration response
* to be rooted at this cert.
* @property publishNotaries Hook for a network map server to capture the generated [NotaryInfo] objects needed for
* creating the network parameters. This is needed as the network map server is expected to distribute it. The callback
* will occur on a different thread to the driver-calling thread.
* @property rootCert If specified then the nodes will register themselves with the doorman service using [url] and expect
* the registration response to be rooted at this cert. If not specified then no registration is performed and the dev
* root cert is used as normal.
*/
data class CompatibilityZoneParams(val url: URL, val rootCert: X509Certificate? = null)
data class CompatibilityZoneParams(val url: URL, val publishNotaries: (List<NotaryInfo>) -> Unit, val rootCert: X509Certificate? = null)
fun <A> internalDriver(
isDebug: Boolean = DriverParameters().isDebug,
@ -875,8 +971,7 @@ fun <A> internalDriver(
}
fun getTimestampAsDirectoryName(): String {
// Add a random number in case 2 tests are started in the same instant.
return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC).format(Instant.now()) + random63BitValue()
return DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss.SSS").withZone(UTC).format(Instant.now())
}
fun writeConfig(path: Path, filename: String, config: Config) {

View File

@ -1,22 +1,22 @@
package net.corda.testing.node.internal.network
import net.corda.core.crypto.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.cert
import net.corda.core.internal.toX509CertHolder
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.network.DigitalSignatureWithCert
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.SignedNetworkMap
import net.corda.nodeapi.internal.*
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.testing.ROOT_CA
import org.bouncycastle.asn1.x500.X500Name
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.server.handler.HandlerCollection
@ -36,26 +36,36 @@ import javax.ws.rs.core.Response.ok
class NetworkMapServer(cacheTimeout: Duration,
hostAndPort: NetworkHostAndPort,
root_ca: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing.
rootCa: CertificateAndKeyPair = ROOT_CA, // Default to ROOT_CA for testing.
private val myHostNameValue: String = "test.host.name",
vararg additionalServices: Any) : Closeable {
companion object {
val stubNetworkParameter = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10)
private val serializedParameters = stubNetworkParameter.serialize()
private val stubNetworkParameters = NetworkParameters(1, emptyList(), 10485760, 40000, Instant.now(), 10)
private fun networkMapKeyAndCert(rootCAKeyAndCert: CertificateAndKeyPair): CertificateAndKeyPair {
val networkMapKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val networkMapCert = X509Utilities.createCertificate(
CertificateType.INTERMEDIATE_CA,
CertificateType.NETWORK_MAP,
rootCAKeyAndCert.certificate,
rootCAKeyAndCert.keyPair,
X500Name("CN=Corda Network Map,L=London"),
CordaX500Name("Corda Network Map", "R3 Ltd", "London","GB"),
networkMapKey.public).cert
// Check that the certificate validates. Nodes will perform this check upon receiving a network map,
// it's better to fail here than there.
X509Utilities.validateCertificateChain(rootCAKeyAndCert.certificate.cert, networkMapCert)
return CertificateAndKeyPair(networkMapCert.toX509CertHolder(), networkMapKey)
}
}
private val server: Server
private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(root_ca))
var networkParameters: NetworkParameters = stubNetworkParameters
set(networkParameters) {
check(field == stubNetworkParameters) { "Network parameters can be set only once" }
field = networkParameters
}
private val serializedParameters get() = networkParameters.serialize()
private val service = InMemoryNetworkMapService(cacheTimeout, networkMapKeyAndCert(rootCa))
init {
server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
@ -95,13 +105,13 @@ class NetworkMapServer(cacheTimeout: Duration,
}
@Path("network-map")
class InMemoryNetworkMapService(private val cacheTimeout: Duration, private val networkMapKeyAndCert: CertificateAndKeyPair) {
inner class InMemoryNetworkMapService(private val cacheTimeout: Duration,
private val networkMapKeyAndCert: CertificateAndKeyPair) {
private val nodeInfoMap = mutableMapOf<SecureHash, SignedNodeInfo>()
private val parametersHash = serializedParameters.hash
private val signedParameters = SignedData(
private val parametersHash by lazy { serializedParameters.hash }
private val signedParameters by lazy { SignedData(
serializedParameters,
DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes))
)
DigitalSignature.WithKey(networkMapKeyAndCert.keyPair.public, Crypto.doSign(networkMapKeyAndCert.keyPair.private, serializedParameters.bytes))) }
@POST
@Path("publish")
@ -151,7 +161,7 @@ class NetworkMapServer(cacheTimeout: Duration,
@GET
@Path("my-hostname")
fun getHostName(): Response {
return Response.ok("test.host.name").build()
return Response.ok(myHostNameValue).build()
}
}
}

View File

@ -5,27 +5,24 @@ package net.corda.testing
import net.corda.core.contracts.PartyAndReference
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.cert
import net.corda.core.internal.unspecifiedCountry
import net.corda.core.internal.x500Name
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.config.configureDevKeyAndTrustStores
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.createDevNodeCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.cert.X509CertificateHolder
import java.math.BigInteger
import java.nio.file.Files
import java.security.KeyPair
import java.security.PublicKey
import java.util.concurrent.atomic.AtomicInteger
@ -78,29 +75,11 @@ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List<NetworkHostAnd
return (0 until numberToAlloc).map { NetworkHostAndPort(hostName, freePort + it) }
}
fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration = object : SSLConfiguration {
override val certificatesDirectory = Files.createTempDirectory("certs")
override val keyStorePassword: String get() = "cordacadevpass"
override val trustStorePassword: String get() = "trustpass"
init {
configureDevKeyAndTrustStores(legalName)
}
}
fun getTestPartyAndCertificate(party: Party): PartyAndCertificate {
val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT
val intermediate: CertificateAndKeyPair = DEV_CA
val nodeCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val nodeCaCert = X509Utilities.createCertificate(
CertificateType.NODE_CA,
intermediate.certificate,
intermediate.keyPair,
party.name,
nodeCaKeyPair.public,
nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf()))
val (nodeCaCert, nodeCaKeyPair) = createDevNodeCa(intermediate, party.name)
val identityCert = X509Utilities.createCertificate(
CertificateType.LEGAL_IDENTITY,

View File

@ -1,10 +1,19 @@
package net.corda.testing.internal
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.loggerFor
import net.corda.node.services.config.configureDevKeyAndTrustStores
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.createDevNodeCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.serialization.amqp.AMQP_ENABLED
import org.mockito.Mockito
import org.mockito.internal.stubbing.answers.ThrowsException
import java.lang.reflect.Modifier
import java.nio.file.Files
import java.util.*
@Suppress("unused")
@ -42,3 +51,56 @@ fun <T> rigorousMock(clazz: Class<T>): T = Mockito.mock(clazz) {
it.callRealMethod()
}
}
fun configureTestSSL(legalName: CordaX500Name): SSLConfiguration {
return object : SSLConfiguration {
override val certificatesDirectory = Files.createTempDirectory("certs")
override val keyStorePassword: String get() = "cordacadevpass"
override val trustStorePassword: String get() = "trustpass"
init {
configureDevKeyAndTrustStores(legalName)
}
}
}
private val defaultRootCaName = CordaX500Name("Corda Root CA", "R3 Ltd", "London", "GB")
private val defaultIntermediateCaName = CordaX500Name("Corda Intermediate CA", "R3 Ltd", "London", "GB")
/**
* Returns a pair of [CertificateAndKeyPair]s, the first being the root CA and the second the intermediate CA.
* @param rootCaName The subject name for the root CA cert.
* @param intermediateCaName The subject name for the intermediate CA cert.
*/
fun createDevIntermediateCaCertPath(
rootCaName: CordaX500Name = defaultRootCaName,
intermediateCaName: CordaX500Name = defaultIntermediateCaName
): Pair<CertificateAndKeyPair, CertificateAndKeyPair> {
val rootKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCert = X509Utilities.createSelfSignedCACertificate(rootCaName, rootKeyPair)
val intermediateCaKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCaCert = X509Utilities.createCertificate(
CertificateType.INTERMEDIATE_CA,
rootCert,
rootKeyPair,
intermediateCaName,
intermediateCaKeyPair.public)
return Pair(CertificateAndKeyPair(rootCert, rootKeyPair), CertificateAndKeyPair(intermediateCaCert, intermediateCaKeyPair))
}
/**
* Returns a triple of [CertificateAndKeyPair]s, the first being the root CA, the second the intermediate CA and the third
* the node CA.
* @param legalName The subject name for the node CA cert.
*/
fun createDevNodeCaCertPath(
legalName: CordaX500Name,
rootCaName: CordaX500Name = defaultRootCaName,
intermediateCaName: CordaX500Name = defaultIntermediateCaName
): Triple<CertificateAndKeyPair, CertificateAndKeyPair, CertificateAndKeyPair> {
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath(rootCaName, intermediateCaName)
val nodeCa = createDevNodeCa(intermediateCa, legalName)
return Triple(rootCa, intermediateCa, nodeCa)
}

View File

@ -6,7 +6,10 @@ configurations {
runtimeArtifacts
}
// TODO Fix SLF4J warnings that occur when running the bootstrapper
dependencies {
compile "org.slf4j:slf4j-nop:$slf4j_version"
}
task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').compileJava) {
applicationClass 'net.corda.nodeapi.internal.network.NetworkBootstrapper'
archiveName "network-bootstrapper.jar"
@ -16,6 +19,9 @@ task buildBootstrapperJar(type: FatCapsule, dependsOn: project(':node-api').comp
minJavaVersion = '1.8.0'
jvmArgs = ['-XX:+UseG1GC']
}
from(project(':node:capsule').tasks['buildCordaJAR']) {
rename 'corda-(.*)', 'corda.jar'
}
applicationSource = files(
project(':node-api').configurations.runtime,
project(':node-api').jar

View File

@ -13,7 +13,7 @@ import net.corda.demobench.pty.R3Pty
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.nodeapi.internal.IdentityGenerator
import net.corda.nodeapi.internal.DevIdentityGenerator
import tornadofx.*
import java.io.IOException
import java.lang.management.ManagementFactory
@ -153,7 +153,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
// Generate notary identity and save it into node's directory. This identity will be used in network parameters.
private fun getNotaryIdentity(config: NodeConfigWrapper): Party {
return IdentityGenerator.generateNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName)
return DevIdentityGenerator.installKeyStoreWithNodeIdentity(config.nodeDir, config.nodeConfig.myLegalName)
}
fun reset() {